diff --git a/.env.example b/.env.example index 318b171e1..22dca3adb 100644 --- a/.env.example +++ b/.env.example @@ -59,4 +59,11 @@ TURBO_TELEMETRY_DISABLED=1 # ENABLE_KUBERNETES=true # Enable mock integration -UNSAFE_ENABLE_MOCK_INTEGRATION=true \ No newline at end of file +UNSAFE_ENABLE_MOCK_INTEGRATION=true + +# Enable demo mode (seeds a demo user "demo"/"demo", a sample board and a mock integration) +# DEMO_MODE=true + +# Whether demo mode is read-only. Defaults to true (the public demo blocks all mutations). +# Set to false for writable preview deployments so the demo user can modify the dashboard. +# DEMO_READ_ONLY=false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index fbcd9d5c1..42f647d87 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -33,6 +33,9 @@ body: options: # The below comment is used to insert a new version with on-release.yml #NEXT_VERSION# + - 1.70.0 + - 1.69.2 + - 1.69.0 - 1.68.0 - 1.67.0 - 1.66.1 diff --git a/.github/actions/prebuilt-debian/action.yaml b/.github/actions/prebuilt-debian/action.yaml index af1b7fdad..6e8b04846 100644 --- a/.github/actions/prebuilt-debian/action.yaml +++ b/.github/actions/prebuilt-debian/action.yaml @@ -11,7 +11,7 @@ outputs: runs: using: "composite" steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - name: Set up Docker Buildx diff --git a/.github/workflows/automatic-approval.yml b/.github/workflows/automatic-approval.yml index 6647bad82..9e246f838 100644 --- a/.github/workflows/automatic-approval.yml +++ b/.github/workflows/automatic-approval.yml @@ -17,7 +17,7 @@ jobs: if: github.actor_id == 158783068 || github.actor_id == 190541745 || github.actor_id == 210161987 || github.actor_id == 175486441 steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Obtain token id: obtainToken uses: actions/create-github-app-token@v3 diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 8a01559a9..48d017cde 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -25,13 +25,13 @@ env: jobs: lint-format-autofix: - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.id == github.repository_id runs-on: ubuntu-latest timeout-minutes: 5 permissions: contents: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 0 ref: ${{ github.head_ref }} @@ -60,7 +60,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 0 @@ -78,7 +78,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 0 @@ -92,7 +92,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 0 @@ -106,7 +106,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup uses: ./tooling/github/setup @@ -124,7 +124,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup uses: ./tooling/github/setup - name: Set up Docker Buildx @@ -153,7 +153,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 0 - name: Setup diff --git a/.github/workflows/crowdin-schedule-download.yml b/.github/workflows/crowdin-schedule-download.yml index a5dec8869..22cb2b943 100644 --- a/.github/workflows/crowdin-schedule-download.yml +++ b/.github/workflows/crowdin-schedule-download.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Obtain token id: obtainToken diff --git a/.github/workflows/crowdin-upload.yml b/.github/workflows/crowdin-upload.yml index 4f8d48de6..25e9b44c3 100644 --- a/.github/workflows/crowdin-upload.yml +++ b/.github/workflows/crowdin-upload.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Upload Crowdin translations uses: crowdin/github-action@v2 diff --git a/.github/workflows/deployment-beta-release.yml b/.github/workflows/deployment-beta-release.yml index fd79d1502..ba3bde3a2 100644 --- a/.github/workflows/deployment-beta-release.yml +++ b/.github/workflows/deployment-beta-release.yml @@ -26,7 +26,7 @@ jobs: with: args: "Beta release has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 - name: Get Next Version diff --git a/.github/workflows/deployment-docker-image.yml b/.github/workflows/deployment-docker-image.yml index 359ca09d7..3ab3a758e 100644 --- a/.github/workflows/deployment-docker-image.yml +++ b/.github/workflows/deployment-docker-image.yml @@ -57,7 +57,7 @@ jobs: client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }} permission-contents: write # required to commit package.json & changelog changes, merge them to dev / next and publish the release - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 if: env.SKIP_RELEASE == 'false' with: persist-credentials: false @@ -66,7 +66,7 @@ jobs: - uses: actions/setup-node@v6 if: env.SKIP_RELEASE == 'false' with: - node-version: 24.17.0 + node-version: 24.18.0 package-manager-cache: false - run: npm i -g pnpm if: env.SKIP_RELEASE == 'false' @@ -119,7 +119,7 @@ jobs: outputs: digest: ${{ steps.build.outputs.digest }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: ref: ${{ needs.release.outputs.git_ref }} @@ -158,7 +158,7 @@ jobs: outputs: digest: ${{ steps.build.outputs.digest }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: ref: ${{ needs.release.outputs.git_ref }} diff --git a/.github/workflows/deployment-docs.yml b/.github/workflows/deployment-docs.yml index 7e4aa4387..eeb53d101 100644 --- a/.github/workflows/deployment-docs.yml +++ b/.github/workflows/deployment-docs.yml @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup uses: ./tooling/github/setup - name: Build docs diff --git a/.github/workflows/deployment-weekly-release.yml b/.github/workflows/deployment-weekly-release.yml index 7c5c8d21d..cafa7b30d 100644 --- a/.github/workflows/deployment-weekly-release.yml +++ b/.github/workflows/deployment-weekly-release.yml @@ -29,7 +29,7 @@ jobs: with: args: "Automatic release has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 - name: Get Next Version diff --git a/.github/workflows/docs-algolia-recrawl.yml b/.github/workflows/docs-algolia-recrawl.yml index 63b335985..1d8bdd42d 100644 --- a/.github/workflows/docs-algolia-recrawl.yml +++ b/.github/workflows/docs-algolia-recrawl.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup uses: ./tooling/github/setup diff --git a/.github/workflows/docs-update-contributors.yml b/.github/workflows/docs-update-contributors.yml index 8e8aeac10..bbfbc10cb 100644 --- a/.github/workflows/docs-update-contributors.yml +++ b/.github/workflows/docs-update-contributors.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: ref: dev diff --git a/.github/workflows/docs-update-openapi.yml b/.github/workflows/docs-update-openapi.yml index e8a1192a5..a2f1f7c30 100644 --- a/.github/workflows/docs-update-openapi.yml +++ b/.github/workflows/docs-update-openapi.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: ref: dev diff --git a/.github/workflows/on-pr-prebuilt-debian-validate.yml b/.github/workflows/on-pr-prebuilt-debian-validate.yml index 025f513e3..a67dd1d62 100644 --- a/.github/workflows/on-pr-prebuilt-debian-validate.yml +++ b/.github/workflows/on-pr-prebuilt-debian-validate.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Validate prebuilt dependencies for amd64 id: validate-amd64 uses: ./.github/actions/prebuilt-debian @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-24.04-arm timeout-minutes: 5 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Validate prebuilt dependencies for arm64 id: validate-arm64 uses: ./.github/actions/prebuilt-debian diff --git a/.github/workflows/on-pr-renovate-validate.yml b/.github/workflows/on-pr-renovate-validate.yml index cd5b27c49..4a77b1175 100644 --- a/.github/workflows/on-pr-renovate-validate.yml +++ b/.github/workflows/on-pr-renovate-validate.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 2 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - run: | npx --yes --package renovate -- \ renovate-config-validator --strict .github/renovate.json5 diff --git a/.github/workflows/on-release.yml b/.github/workflows/on-release.yml index 514f225b4..644f4a74d 100644 --- a/.github/workflows/on-release.yml +++ b/.github/workflows/on-release.yml @@ -52,7 +52,7 @@ jobs: permission-contents: write # required to commit to branch permission-pull-requests: write # required to create pr & enable automerge - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: token: ${{ steps.obtainToken.outputs.token }} ref: dev diff --git a/.github/workflows/update-contributors.yml b/.github/workflows/update-contributors.yml index 925e490c7..bb4364596 100644 --- a/.github/workflows/update-contributors.yml +++ b/.github/workflows/update-contributors.yml @@ -29,7 +29,7 @@ jobs: permission-pull-requests: write # required to create pr & enable automerge - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 env: GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }} diff --git a/.github/workflows/update-integration-list.yml b/.github/workflows/update-integration-list.yml index 05bb05d27..d6fdcab73 100644 --- a/.github/workflows/update-integration-list.yml +++ b/.github/workflows/update-integration-list.yml @@ -28,7 +28,7 @@ jobs: permission-contents: write # required to commit to branch permission-pull-requests: write # required to create pr & enable automerge - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 env: GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }} - name: Setup diff --git a/.gitignore b/.gitignore index 1bb62e979..c22262f6d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # dependencies node_modules +.pnpm-store/ .pnp .pnp.js .idea/ @@ -70,7 +71,10 @@ apps/nextjs/public/images/background.png # personal agent config .opencode +.agents/ +skills-lock.json # docusaurus apps/docs/.docusaurus apps/docs/build +.pnpm-store diff --git a/.mise.toml b/.mise.toml index 0b18e941c..d6bd41fc8 100644 --- a/.mise.toml +++ b/.mise.toml @@ -1,2 +1,2 @@ [tools] -node = "24.17.0" +node = "24.18.0" diff --git a/.nvmrc b/.nvmrc index 1dd37d537..ca5c35005 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -24.17.0 +24.18.0 diff --git a/.vscode/extensions.json b/.vscode/extensions.json index f0cdbe63c..8f1b6afce 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,7 @@ { "recommendations": [ "dbaeumer.vscode-eslint", - "esbenp.prettier-vscode", + "oxc.oxc-vscode", "yoavbls.pretty-ts-errors", "million.million-lint", "lokalise.i18n-ally" diff --git a/CHANGELOG.md b/CHANGELOG.md index a37858942..4a6d38619 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,57 @@ +## [1.70.0](https://github.com/homarr-labs/homarr/compare/v1.69.2...v1.70.0) (2026-07-10) + +## [1.69.2](https://github.com/homarr-labs/homarr/compare/v1.69.1...v1.69.2) (2026-07-06) + +### Bug Fixes + +* **layout:** raise AppShellHeader z-index above Indicator dots ([687edb9](https://github.com/homarr-labs/homarr/commit/687edb9226307cb83986097abb24fe7f672e1778)), closes [#6194](https://github.com/homarr-labs/homarr/issues/6194) + +## [1.69.1](https://github.com/homarr-labs/homarr/compare/v1.69.0...v1.69.1) (2026-07-04) + +### Bug Fixes + +* unblock query cache mutations in demo mode and add cache buster ([#6176](https://github.com/homarr-labs/homarr/issues/6176)) ([a382c75](https://github.com/homarr-labs/homarr/commit/a382c75968e73bafff55b5890b1e95f0779b50d3)) + +## [1.69.0](https://github.com/homarr-labs/homarr/compare/v1.68.1...v1.69.0) (2026-07-03) + +### Features + +* add per-widget secret storage for release provider authentication ([77884a3](https://github.com/homarr-labs/homarr/commit/77884a31794e12a6f7bc59d96c8f61aa429ca68d)) +* **config:** enable pnpm global virtual store for worktree support ([#6098](https://github.com/homarr-labs/homarr/issues/6098)) ([3864727](https://github.com/homarr-labs/homarr/commit/3864727502a64052c4e3838468094b30bf04a133)) +* **docker-widget:** add per-column visibility toggle ([#6093](https://github.com/homarr-labs/homarr/issues/6093)) ([0765262](https://github.com/homarr-labs/homarr/commit/076526293d13379004461633d7ba4c2e83bd5ffe)) +* **docs:** addd in documentation for archiveteam warrior ([f66469d](https://github.com/homarr-labs/homarr/commit/f66469d79797a2da214588f4e8253b03633c59a5)) +* simplify caching — TanStack Query as single source of truth ([#6075](https://github.com/homarr-labs/homarr/issues/6075)) ([f19033e](https://github.com/homarr-labs/homarr/commit/f19033e0d53d248aed678737f3d04d5d8c7815b5)) +* **widgets:** right-click context menu with auto-generated toggle actions ([#6126](https://github.com/homarr-labs/homarr/issues/6126)) ([7e58858](https://github.com/homarr-labs/homarr/commit/7e58858353ae866efd657270d4bb16d2c7d3e551)) +* **widgets:** show calendar events on hover with pin on click ([#2637](https://github.com/homarr-labs/homarr/issues/2637)) ([#6081](https://github.com/homarr-labs/homarr/issues/6081)) ([db3d588](https://github.com/homarr-labs/homarr/commit/db3d58821e4de8d46b110125d0fa36919765248f)) + +### Bug Fixes + +* add id to repository schema and handle missing ids gracefully ([8747d5f](https://github.com/homarr-labs/homarr/commit/8747d5f93ee9a295aad3c7035e8c6c7080bf5ee6)) +* address PR review - atomic upsert, MCP metadata, stale tokens, save UX ([757d598](https://github.com/homarr-labs/homarr/commit/757d59854e5ec82d91c053b8bc3d7eaf2e70efc4)) +* address PR review findings for widget-only integration refactor ([c892731](https://github.com/homarr-labs/homarr/commit/c892731479b47fab6218bd1f52fa6eccbdaa371e)) +* address review - paginate GH releases, GitLab tag_name, docs, test ([e687bc8](https://github.com/homarr-labs/homarr/commit/e687bc8c9277797acaa164a09c74b3808a2cb346)) +* apply version filter to LinuxServer.io provider ([34d976e](https://github.com/homarr-labs/homarr/commit/34d976eeea09e396334ce8fce5c492c07e7dd32b)) +* catch individual repo fetch failures to prevent batch crashes ([d893515](https://github.com/homarr-labs/homarr/commit/d893515b932608ea08c9c6e90b83f340e40f6f8e)) +* compile versionRegex once and add fetch timeouts ([64c7512](https://github.com/homarr-labs/homarr/commit/64c75122833981a49e9c8bf1789d501faff6d62d)) +* **deps:** update dependency postcss to ^8.5.16 ([#6169](https://github.com/homarr-labs/homarr/issues/6169)) ([9252fdc](https://github.com/homarr-labs/homarr/commit/9252fdc13971bd982224a31d7be577112f99ec45)) +* dns-hole widgets showing stale disconnected state ([#6096](https://github.com/homarr-labs/homarr/issues/6096)) ([2443f1c](https://github.com/homarr-labs/homarr/commit/2443f1cd3ff6765a91783d60d42aa03d9ba63fcd)) +* gracefully degrade when widget_secret table is missing or access denied ([30107e4](https://github.com/homarr-labs/homarr/commit/30107e4e76a454ff7402de9a8bc2e0dd6f422aa8)) +* handle missing widgetSecrets table and add default releases repo ([af35a50](https://github.com/homarr-labs/homarr/commit/af35a50fa05fee2f0118e98727eb108b0200f762)) +* **immich:** v3 API compatibility — use searchAssets for album assets, drop deviceAssetId ([#5977](https://github.com/homarr-labs/homarr/issues/5977)) ([a5ec880](https://github.com/homarr-labs/homarr/commit/a5ec880eec276e0814d2ab3ed7f9bc0b804d0465)) +* quick wins — calendar, about, glances, admin UX, releases, iframe, notes ([#6116](https://github.com/homarr-labs/homarr/issues/6116)) ([f9da71a](https://github.com/homarr-labs/homarr/commit/f9da71a19bdfa589aafe26b164d0e0782208ac0a)) +* restore ArchiveTeam Warrior as a proper integration ([8a5ad58](https://github.com/homarr-labs/homarr/commit/8a5ad5861ba4c2d0978f1fa3f6115a1c5974f8a2)) +* **timetable:** restore station search by removing disabled query guard ([d670b69](https://github.com/homarr-labs/homarr/commit/d670b6939672932c1a3d17ab9913901491cb46e3)) +* update CHANGES.md for ArchiveTeam Warrior restoration, remove typo translation key ([62baaf7](https://github.com/homarr-labs/homarr/commit/62baaf78130c4b74e899a948cee17097d8c02593)) +* verbose error messages in all release providers ([2155f13](https://github.com/homarr-labs/homarr/commit/2155f13f245f601b99458e227b6e058fbfa44cd9)) +* **widgets:** make downloads widget table background transparent ([#6025](https://github.com/homarr-labs/homarr/issues/6025)) ([73bed51](https://github.com/homarr-labs/homarr/commit/73bed51df895250f415efd7a87e721e403b3fd99)) + +## [1.68.1](https://github.com/homarr-labs/homarr/compare/v1.68.0...v1.68.1) (2026-06-30) + +### Bug Fixes + +* make source maps opt-in to restore v1.67.0 memory baseline ([57ef0af](https://github.com/homarr-labs/homarr/commit/57ef0af4a252ea39727caeba7e13587dabc6254e)), closes [#6123](https://github.com/homarr-labs/homarr/issues/6123) +* revert production source maps to restore v1.67.0 memory baseline ([60a69e3](https://github.com/homarr-labs/homarr/commit/60a69e31efc7168b8f4309daf75954d7730d883b)), closes [#6123](https://github.com/homarr-labs/homarr/issues/6123) + ## [1.68.0](https://github.com/homarr-labs/homarr/compare/v1.67.0...v1.68.0) (2026-06-26) ### Features diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 000000000..c8a801420 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,96 @@ +# Changes + +## Goal + +Remove widget-only services from Homarr's global Integration system. + +The affected services are release providers and Search.ch. They are only used by specific widgets, so keeping them as global integrations added unnecessary setup, UI, database rows, docs pages, and integration package code. + +ArchiveTeam Warrior remains a proper integration but is no longer auto-seeded on first install. + +## Why + +These services do not need shared integration state. + +- Release providers are selected per Releases widget repository. +- Search.ch is only used by the Timetable widget. + +Moving them into widget options makes configuration simpler and removes dead-weight integration code from `packages/integrations`. + +## What Changed + +### Release Providers + +- Added release provider definitions in `packages/definitions/src/release-provider.ts`. +- Removed release providers from `IntegrationKind`. +- Moved release-fetching code out of `packages/integrations` and into `packages/request-handler/src/release-providers.ts`. +- Updated the Releases widget to store provider configuration directly in widget options: + - `provider` + - `identifier` + - `versionFilter` + - `providerUrl` +- Updated the Releases widget editor to use a provider select instead of an integration select. +- Updated Docker import mapping so `ghcr.io` maps to `gitHubContainerRegistry`. +- GitHub Container Registry now uses the public OCI registry API and accepts full image references such as `ghcr.io/homarr-labs/homarr:latest`. + +### Timetable / Search.ch + +- Removed Search.ch as an integration. +- Added a Timetable widget `baseUrl` option, defaulting to `https://search.ch`. +- Moved Search.ch request/parsing logic into `packages/request-handler/src/timetable.ts`. +- Updated the Timetable API to receive `baseUrl` directly from widget options. + +### ArchiveTeam Warrior + +- Kept as a proper integration (not removed). +- Removed `defaultUrl` from the integration definition to prevent auto-seeding on first install. + +### Integration Cleanup + +- Removed creators, exports, docs references, and implementation files for the deleted integrations. +- Removed unused dependencies from `packages/integrations/package.json`. + +### Database Migration + +- Added `packages/db/migrations/custom/0003_remove_widget_only_integrations.ts`. +- The migration moves old integration-linked widget config into widget options, then deletes the old widget-only integration rows. +- Updated the earlier release widget migration to use the new `provider` field instead of `providerIntegrationId`. + +### Docs + +- Updated Releases widget docs to describe providers instead of integrations. +- Updated Timetable docs to remove Search.ch integration setup. +- Removed obsolete integration docs pages for the deleted integration kinds. +- Updated docs sitemap and integration docs mappings. + +### Translations + +- Added English labels for the new widget options: + - `widget.timetable.option.baseUrl.label` + - `widget.releases.option.repositories.providerUrl.label` + +## How It Works Now + +Release provider configuration lives on each Releases widget repository entry. The request handler resolves provider metadata and fetches releases directly from the provider-specific endpoint. GHCR uses anonymous OCI registry access for public packages. + +Timetable passes its widget URL option directly to its API router and request handler. No global integration lookup is needed. + +ArchiveTeam Warrior continues to use the standard integration system, but users must manually add the integration (it is not auto-created on install). + +## Deliberate Non-Goals + +- No provider secret/token support was added. +- No new secret storage model was introduced. +- No compatibility layer was kept in `packages/integrations`. + +Widget options are client-visible, so storing provider credentials there would be wrong. If authenticated provider requests are needed later, they need a separate secret-storage design. + +## Verification So Far + +- Focused typechecks passed for affected packages. +- Full `pnpm turbo typecheck` passed. +- Full `pnpm lint` passed with existing warnings. +- Focused release provider normalization tests passed. +- Widget translation test passed. +- Live GHCR smoke checks passed for `ghcr.io/muchobien/pocketbase:latest` and `ghcr.io/homarr-labs/homarr:latest`. +- `pnpm test packages/integrations/test/pi-hole.spec.ts` passed earlier, but failed on rebased reruns because Pi-hole reported `domainsBeingBlocked` as `-2`/`0` instead of `> 1`. diff --git a/CODEOWNERS b/CODEOWNERS index b3e2214ac..7082b9bac 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,12 +1,220 @@ # ── CODEOWNERS ─────────────────────────────────────── * @homarr-labs/maintainers -# Specific features / integrations -# Glueten -apps/docs/docs/integrations/glueten @Lok3sh-kumar +# Specific features / integrations +# These owners come from the PR that introduced each integration implementation. +# Specific rules override the global fallback because CODEOWNERS uses the last +# matching pattern; do not include @homarr-labs/maintainers here unless they +# should also be requested for that integration. + +# AdGuard Home - introduced in https://github.com/homarr-labs/homarr/pull/929 by @hillaliy +apps/docs/docs/integrations/adguard-home @hillaliy +packages/integrations/src/adguard-home @hillaliy + +# Anchor - introduced in https://github.com/homarr-labs/homarr/pull/5040 by @ohmzi +apps/docs/docs/integrations/anchor @ohmzi +packages/integrations/src/anchor @ohmzi + +# ArchiveTeam Warrior - introduced in https://github.com/homarr-labs/homarr/pull/5784 by @darbyjack +apps/docs/docs/integrations/archive-team-warrior @darbyjack +packages/integrations/src/archive-team-warrior @darbyjack + +# Bazarr - introduced in https://github.com/homarr-labs/homarr/pull/6156 by @Ayush7614 +apps/docs/docs/integrations/bazarr @Ayush7614 +apps/docs/docs/widgets/bazarr @Ayush7614 +packages/api/src/router/widgets/bazarr.ts @Ayush7614 +packages/integrations/src/bazarr @Ayush7614 +packages/request-handler/src/bazarr.ts @Ayush7614 +packages/widgets/src/bazarr @Ayush7614 + +# Audiobookshelf - introduced in https://github.com/homarr-labs/homarr/pull/5910 by @ajnart +apps/docs/docs/integrations/audiobookshelf @ajnart +packages/integrations/src/audiobookshelf @ajnart + +# Beszel - introduced in https://github.com/homarr-labs/homarr/pull/5900 by @ajnart +apps/docs/docs/integrations/beszel @ajnart +packages/integrations/src/beszel @ajnart + +# Codeberg - introduced in https://github.com/homarr-labs/homarr/pull/3253 by @Aandree5 +apps/docs/docs/integrations/codeberg @Aandree5 +packages/integrations/src/codeberg @Aandree5 + +# Coolify - introduced in https://github.com/homarr-labs/homarr/pull/4789 by @Rhiz3K +apps/docs/docs/integrations/coolify @Rhiz3K +packages/integrations/src/coolify @Rhiz3K + +# Dash. - introduced in https://github.com/homarr-labs/homarr/pull/1541 by @manuel-rw +apps/docs/docs/integrations/dash-dot @manuel-rw +packages/integrations/src/dashdot @manuel-rw + +# Docker Hub - introduced in https://github.com/homarr-labs/homarr/pull/3253 by @Aandree5 +apps/docs/docs/integrations/docker-hub @Aandree5 +packages/integrations/src/docker-hub @Aandree5 + +# Emby - introduced in https://github.com/homarr-labs/homarr/pull/2466 by @Meierschlumpf +apps/docs/docs/integrations/emby @Meierschlumpf +packages/integrations/src/emby @Meierschlumpf + +# GitHub - introduced in https://github.com/homarr-labs/homarr/pull/3253 by @Aandree5 +apps/docs/docs/integrations/github @Aandree5 +packages/integrations/src/github @Aandree5 + +# GitHub Container Registry - introduced in https://github.com/homarr-labs/homarr/pull/3727 by @Aandree5 +apps/docs/docs/integrations/github-containerregistry @Aandree5 +packages/integrations/src/github-container-registry @Aandree5 + +# GitLab - introduced in https://github.com/homarr-labs/homarr/pull/3253 by @Aandree5 +apps/docs/docs/integrations/gitlab @Aandree5 +packages/integrations/src/gitlab @Aandree5 + +# Glances - introduced in https://github.com/homarr-labs/homarr/pull/5102 by @manuel-rw +apps/docs/docs/integrations/glances @manuel-rw +packages/integrations/src/glances @manuel-rw + +# Gluetun - introduced in https://github.com/homarr-labs/homarr/pull/5812 by @Lok3sh-kumar +apps/docs/docs/integrations/gluetun @Lok3sh-kumar packages/integrations/src/gluetun @Lok3sh-kumar -# Exempt Renovate‑managed files (no owners) +# Gotify - introduced in https://github.com/homarr-labs/homarr/pull/5847 by @Nerdy-Technician +apps/docs/docs/integrations/gotify @Nerdy-Technician +packages/integrations/src/gotify @Nerdy-Technician + +# Home Assistant - introduced in https://github.com/homarr-labs/homarr/pull/578 by @manuel-rw +apps/docs/docs/integrations/home-assistant @manuel-rw +packages/integrations/src/homeassistant @manuel-rw + +# ICal - introduced in https://github.com/homarr-labs/homarr/pull/3980 by @NicolasNewman +apps/docs/docs/integrations/ical @NicolasNewman +packages/integrations/src/ical @NicolasNewman + +# Immich - introduced in https://github.com/homarr-labs/homarr/pull/5165 by @manuel-rw +apps/docs/docs/integrations/immich @manuel-rw +packages/integrations/src/immich @manuel-rw + +# Jellyfin - introduced in https://github.com/homarr-labs/homarr/pull/672 by @manuel-rw +apps/docs/docs/integrations/jellyfin @manuel-rw +packages/integrations/src/jellyfin @manuel-rw + +# Jellyseerr - introduced in https://github.com/homarr-labs/homarr/pull/774 by @manuel-rw +apps/docs/docs/integrations/jellyseerr @manuel-rw +packages/integrations/src/jellyseerr @manuel-rw + +# LinuxServer.io - introduced in https://github.com/homarr-labs/homarr/pull/3607 by @Aandree5 +apps/docs/docs/integrations/linux-server-io @Aandree5 +packages/integrations/src/linuxserverio @Aandree5 + +# Navidrome - introduced in https://github.com/homarr-labs/homarr/pull/5910 by @ajnart +apps/docs/docs/integrations/navidrome @ajnart +packages/integrations/src/navidrome @ajnart + +# Nextcloud - introduced in https://github.com/homarr-labs/homarr/pull/2501 by @manuel-rw +apps/docs/docs/integrations/nextcloud @manuel-rw +packages/integrations/src/nextcloud @manuel-rw + +# npm - introduced in https://github.com/homarr-labs/homarr/pull/3253 by @Aandree5 +apps/docs/docs/integrations/npm @Aandree5 +packages/integrations/src/npm @Aandree5 + +# ntfy - introduced in https://github.com/homarr-labs/homarr/pull/2900 by @itzTheMeow +apps/docs/docs/integrations/ntfy @ajnart @manuel-rw +packages/integrations/src/ntfy @ajnart @manuel-rw + +# OpenMediaVault - introduced in https://github.com/homarr-labs/homarr/pull/1142 by @hillaliy +apps/docs/docs/integrations/open-media-vault @hillaliy +packages/integrations/src/openmediavault @hillaliy + +# OPNsense - introduced in https://github.com/homarr-labs/homarr/pull/3424 by @Oupsman +apps/docs/docs/integrations/opnsense @Oupsman +packages/integrations/src/opnsense @Oupsman + +# Overseerr - introduced in https://github.com/homarr-labs/homarr/pull/774 by @manuel-rw +apps/docs/docs/integrations/overseerr @manuel-rw +packages/integrations/src/overseerr @manuel-rw + +# Paperless-ngx - introduced in https://github.com/homarr-labs/homarr/pull/5910 by @ajnart +apps/docs/docs/integrations/paperless-ngx @ajnart +packages/integrations/src/paperless-ngx @ajnart + +# PatchMon - introduced in https://github.com/homarr-labs/homarr/pull/6119 by @mrjoshuap +apps/docs/docs/integrations/patchmon @mrjoshuap +packages/integrations/src/patchmon @mrjoshuap +packages/widgets/src/patchmon @mrjoshuap +packages/api/src/router/widgets/patchmon.ts @mrjoshuap +packages/request-handler/src/patchmon.ts @mrjoshuap + +# PeaNUT - introduced in https://github.com/homarr-labs/homarr/pull/5862 by @afonsojramos +apps/docs/docs/integrations/peanut @afonsojramos +packages/integrations/src/peanut @afonsojramos + +# Pi-hole - introduced in https://github.com/homarr-labs/homarr/pull/521 by @Meierschlumpf +apps/docs/docs/integrations/pi-hole @Meierschlumpf +packages/integrations/src/pi-hole @Meierschlumpf + +# Plex - introduced in https://github.com/homarr-labs/homarr/pull/1342 by @hillaliy +apps/docs/docs/integrations/plex @hillaliy +packages/integrations/src/plex @hillaliy + +# Prowlarr - introduced in https://github.com/homarr-labs/homarr/pull/965 by @hillaliy +apps/docs/docs/integrations/prowlarr @hillaliy +packages/integrations/src/prowlarr @hillaliy + +# Proxmox - introduced in https://github.com/homarr-labs/homarr/pull/1969 by @Meierschlumpf +apps/docs/docs/integrations/proxmox @Meierschlumpf +packages/integrations/src/proxmox @Meierschlumpf + +# Quay - introduced in https://github.com/homarr-labs/homarr/pull/3607 by @Aandree5 +apps/docs/docs/integrations/quay @Aandree5 +packages/integrations/src/quay @Aandree5 + +# search.ch - introduced in https://github.com/homarr-labs/homarr/pull/5180 by @Meierschlumpf +apps/docs/docs/integrations/search-ch @Meierschlumpf +packages/integrations/src/search-ch @Meierschlumpf + +# Speedtest Tracker - introduced in https://github.com/homarr-labs/homarr/pull/5363 by @kylegrieder +apps/docs/docs/integrations/speedtest-tracker @kylegrieder +packages/integrations/src/speedtest-tracker @kylegrieder + +# Synology - introduced in https://github.com/homarr-labs/homarr/pull/6121 by @mrjoshuap +apps/docs/docs/integrations/synology @mrjoshuap +packages/integrations/src/synology @mrjoshuap + +# Technitium DNS - introduced in https://github.com/homarr-labs/homarr/pull/5832 by @lopsch +apps/docs/docs/integrations/technitium-dns @lopsch +packages/integrations/src/technitium @lopsch + +# Tracearr - introduced in https://github.com/homarr-labs/homarr/pull/5060 by @StrandedTurtle +apps/docs/docs/integrations/tracearr @StrandedTurtle +packages/integrations/src/tracearr @StrandedTurtle + +# TrueNAS - introduced in https://github.com/homarr-labs/homarr/pull/3745 by @manuel-rw +apps/docs/docs/integrations/truenas @manuel-rw @afonsojramos +packages/integrations/src/truenas @manuel-rw @afonsojramos + +# Umami - introduced in https://github.com/homarr-labs/homarr/pull/5362 by @febrile42 +apps/docs/docs/integrations/umami @febrile42 +packages/integrations/src/umami @febrile42 + +# UniFi Controller - introduced in https://github.com/homarr-labs/homarr/pull/2236 by @pitschi +apps/docs/docs/integrations/unifi-controller @pitschi +packages/integrations/src/unifi-controller @pitschi + +# Unraid - introduced in https://github.com/homarr-labs/homarr/pull/4439 by @manuel-rw +apps/docs/docs/integrations/unraid @manuel-rw +packages/integrations/src/unraid @manuel-rw + +# Uptime Kuma - introduced in https://github.com/homarr-labs/homarr/pull/5910 by @ajnart +apps/docs/docs/integrations/uptime-kuma @ajnart +packages/integrations/src/uptime-kuma @ajnart + +# Traefik - by @Nerdy-Technician +apps/docs/docs/integrations/traefik @Nerdy-Technician +apps/docs/docs/widgets/traefik @Nerdy-Technician +packages/api/src/router/widgets/traefik.ts @Nerdy-Technician +packages/integrations/src/traefik @Nerdy-Technician +packages/request-handler/src/traefik.ts @Nerdy-Technician +packages/widgets/src/traefik @Nerdy-Technician + +# Exempt Renovate-managed files (no owners) package.json **/package.json package-lock.json diff --git a/Dockerfile b/Dockerfile index d50756e39..e7b5b679c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:24.17.0-alpine AS base +FROM node:24.18.0-alpine AS base FROM base AS builder RUN apk add --no-cache libc6-compat @@ -20,17 +20,6 @@ ARG DISABLE_REDIS_LOGS='true' RUN corepack enable pnpm && pnpm build -# The standalone output tracing omits server chunk source maps (.next/server/**/*.map), which is -# where production Node stack frames point. Copy the .map files into the standalone tree (preserving -# directory structure) so the existing standalone COPY ships them, keeping traces readable. -# See https://github.com/homarr-labs/homarr/issues/5891 -RUN cd apps/nextjs/.next/server && \ - find . -name '*.map' | while IFS= read -r map; do \ - dest="../standalone/apps/nextjs/.next/server/$map"; \ - mkdir -p "$(dirname "$dest")"; \ - cp "$map" "$dest"; \ - done - FROM base AS runner WORKDIR /app @@ -53,6 +42,7 @@ RUN mkdir -p /var/cache/nginx && \ COPY --from=builder /app/apps/nextjs/next.config.ts . COPY --from=builder /app/apps/nextjs/package.json . +COPY --from=builder /app/pnpm-workspace.yaml ./pnpm-workspace.yaml COPY --from=builder /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node /app/build/better_sqlite3.node @@ -75,9 +65,6 @@ ENV DB_DRIVER='better-sqlite3' ENV AUTH_PROVIDERS='credentials' ENV REDIS_IS_EXTERNAL='false' ENV NODE_ENV='production' -# Tell Node to symbolicate stack traces using the shipped .map files so production logs -# show original source locations instead of minified frames, see https://github.com/homarr-labs/homarr/issues/5891 -ENV NODE_OPTIONS='--enable-source-maps' ENTRYPOINT [ "/app/entrypoint.sh" ] CMD ["sh", "run.sh"] diff --git a/apps/docs/docs/getting-started/after-the-installation.mdx b/apps/docs/docs/getting-started/after-the-installation.mdx index 5f923cec8..7ac904bf4 100644 --- a/apps/docs/docs/getting-started/after-the-installation.mdx +++ b/apps/docs/docs/getting-started/after-the-installation.mdx @@ -28,6 +28,7 @@ Read our [glossary](/docs/getting-started/glossary) if you are new to Homarr. Congratulations on installing Homarr. Now it's time to get yourself familiar with the concepts of Homarr and how to use it. ## Onboarding + The onboard will show the first time you start Homarr. This process will help you with your first steps and adding some initial data, such as the administrator account. @@ -45,50 +46,68 @@ You can either import a previous dashboard using the ZIP archive or start from s You may refresh anytime and continue from a different device - the onboarding is saved immediately on the server for your convenience. ### Starting from scratch -Depending on your auth provider configuration you will either first see the admin user creation form or the external group creation form. -If credentials provider was selected, you'll be prompted to create an admin user for Homarr. -This user cannot be deleted and has full control over Homarr and all of it's boards. -You must choose an username and password (both can later be changed). -We enforce strict password requirements, when you enter your password, to protect your instance if you accidentially -expose it to the Internet. -{"Screenshot +Depending on your auth provider configuration you will either first see the admin user creation form or the external group creation form. +If the credentials provider was selected, you'll be prompted to create an admin user for Homarr. +This user cannot be deleted and has full control over Homarr and all of its boards. +You must choose a username and password (both can later be changed). +We enforce strict password requirements when you enter your password to protect your instance if you accidentally +expose it to the internet. + +{"Screenshot If you selected external providers (such as LDAP or OIDC), you'll be prompted to create an admin group. -This group will be used to synchronize users from your provider to Homarr. -When a user is logged in through the provider the groups of him that match groups with any in Homarr will be added to the user. -The name of the group has to match exactly the name of the group in the provider. +This group will be used to synchronize users from your provider to Homarr. +When a user logs in through the provider, any of their groups that match groups in Homarr will be assigned to the user. +The name of the group must match exactly the name of the group in the provider. -{"Screenshot +{"Screenshot Next, you can configure some basic settings for Homarr. We recommend to leave all settings at default for most users. Here you can also disable analytics. Please note that our analytics are completly anonymous and is fully compliant with GDPR and PECR. -{"Screenshot +{"Screenshot ### Setting up integrations After configuring server settings, the onboarding guides you through setting up your integrations. #### Docker service discovery + If Docker is available (socket mounted), Homarr automatically detects running containers and matches them to known integration types. Matching integrations are pre-selected in the integration list, saving you from manually picking each one. #### URL templating + You can define a base URL template to auto-fill integration URLs in bulk. Two modes are supported: + - **Subdomain mode** - e.g. `{app}.example.com` where `{app}` is replaced with the integration name - **Port mode** - e.g. `192.168.1.1:{port}` where `{port}` is replaced with the integration's default port #### API key helper links + For integrations that require API keys (such as Sonarr, Radarr, SABnzbd, or Seerr), a helper link opens the relevant settings page in the target application so you can quickly copy your key. #### Creating apps from Docker containers + You can optionally auto-create bookmark apps for all discovered Docker containers, making them immediately available on your board. :::note -Some niche and registry integrations (GitHub, npm, Docker Hub, GitLab, Codeberg, LinuxServer.io, GHCR, Quay, Search.ch) are hidden from the onboarding selection grid but remain available on the manage integrations page. +Release providers and Search.ch are configured directly in their widgets instead of the integrations page. ::: @@ -97,30 +116,41 @@ The onboarding can no longer be started again. From here you can click on one of the provided links to continue your journey. We recommend you to continue with the section [creating your first board on this page](#creating-your-first-board). -{"Screenshot +{"Screenshot ### Importing a ZIP from version before 1.0.0 + This is recommended when you used Homarr before and want to re-import your data. It is important that you first migrate correctly using [the list of breaking changes for 1.0](/blog/2024/09/23/version-1.0). -Export from old Homarr +Export from old Homarr **How to export and import data** -1. Navigate to your old Homarr, open your management pages, open the tools submenu and open ``Migrate to 1.0``. + +1. Navigate to your old Homarr, open your management pages, open the tools submenu and open `Migrate to 1.0`. 2. Check the contents that you want to export. We recommend you to export all data. 3. Click on the export data button 4. Save the ZIP file, that is being downloaded, on your device. If you selected "integrations" or "users", you also get an encryption / decryption key. -Copy it and do not loose it! -5. Go to your new >1.0.0 Homarr instance and start the onboarding. Choose ``Import from Homarr before 1.0`` + Copy it and do not loose it! +5. Go to your new >1.0.0 Homarr instance and start the onboarding. Choose `Import from Homarr before 1.0` 6. Drag and drop your previously downloaded ZIP archive into the file upload:
7. Next, choose the data that you want to import and how to import it. Since screen sizes were removed in 1.0, we create a new board for every screen size by default. We recommend you to only keep the best layout and adjust it later in 1.0.0 - but you can also import all of them and pick later.
8. If you previously exported integrations or users, you will be asked to decrypt the data safely using your obtained encryption token. Paste into the modal and confirm your action.
9. Next, you can [follow the same setup as when starting from scratch](#starting-from-scratch). After you completed the onboarding, all of your data will be migrated and available. ## Creating your first board + In Homarr, you organize your apps and widgets on boards. Boards can be organized using the interactive drag and drop. -Let's create your first board by navigating here: ``Manage`` > ``Boards`` > ``New board``: +Let's create your first board by navigating here: `Manage` > `Boards` > `New board`: ![](./img/manage-create-board-button.png) A board must always have a name and can either be public or private. @@ -133,12 +163,13 @@ Congratulations - you've just created your first board. You can see boards, that ![](./img/manage-view-board.png) ## Add your first app to Homarr + To add links to your dashboard, you must first add them as an "app". Navigate to this page to add an app: ![](./img/manage-navigate-apps.png) -Next, click on the ``New app`` button. You'll need to provide a name and an icon to create an app. +Next, click on the `New app` button. You'll need to provide a name and an icon to create an app. The name must be at least one character long but you can provide any image URL for the icon. Homarr ships with over 11K icons - so you can just search by entering a search text instead. @@ -149,8 +180,9 @@ After you created the app, you'll see in on the apps page: ![](./img/manage-created-app.png) ## Arrange and organize your board + Let's add your freshly created app, from the previous step, to the board. -To do this, edit your previously created board by clicking on the big ``Open board`` button: +To do this, edit your previously created board by clicking on the big `Open board` button: ![](./img/manage-view-board.png) @@ -165,23 +197,38 @@ Here you have the following options in the header: When entering the edit mode, you'll get some additional buttons to work with: -{"Screenshot +{"Screenshot - **Add item**: Click this button to add new elements to your page. - **Exit edit mode**: Click this btuton to save your changes and exit edit mode. To continue, click the option "New Item": -{"Screenshot +{"Screenshot Next, you can choose what item you'd like to add. We will discuss the other options in the following sections - for now choose "App" to continue. -{"Screenshot +{"Screenshot As soon as you chose the app, the modal will close and you'll get the following item on your board: -Screenshot of the No app element on the board +Screenshot of the No app element on the board This is expected, because you haven't told Homarr what app it should show in this item. To configure this, click on the small 3 dots at the top of the item in the edit mode and choose "Edit item" in the menu. @@ -228,17 +275,18 @@ Be sure to save your changes by exiting edit mode! ![](./img/exit-edit-mode.png) ## Adding your first integration + Now it's time to integrate some basic data into your board. To do this, we first need to create an integration. An integration is a connection between Homarr and a third party application - usually something that you run and deploy yourself. -Navigate to ``Manage`` > ``Integrations`` (for example via your user menu). +Navigate to `Manage` > `Integrations` (for example via your user menu). As an example, we are going to use the public demo instance of **Dash.** - a popular application to monitor CPU / hardware usage. Choose the **Dash.** integration from the list of available integrations: ![](./img/choose-integration.gif) -Next, choose a name (or leave it at default), enter ``https://dash.mauz.dev/`` as the URL in the form and click on ``Test connection and create``. +Next, choose a name (or leave it at default), enter `https://dash.mauz.dev/` as the URL in the form and click on `Test connection and create`. Pressing the save button will perform a test request to the integration, to confirm that the networking setup is working as expected. Sometimes invalid configurations, firewalls and other mistakes can lead to connectivity problems - [follow this guide here](/docs/management/integrations/#troubleshooting-connection) if you experience issues. @@ -254,13 +302,14 @@ the target application or your router. ::: ## Embed integration data using widgets + Let's add your integration, that you have configured in the section above, as a widget to your board. First, navigate back to your board that you want to add it to. Next, enter edit mode again and add another item to your board: ![](./img/manage-board-header-add-item-menu.png) -Instead of choosing an app, let's choose ``System Health Monitoring`` this time. +Instead of choosing an app, let's choose `System Health Monitoring` this time. Click on "Add to board": ![](./img/manage-board-header-choose-system-health.png) @@ -290,10 +339,12 @@ As with every change, don't forget to save your changes using the exit edit mode ![](./img/exit-edit-mode.png) ## Grouping your items + As the last step, let's organize your freshly created items using two different options. You can choose either or both of them on your boards. ### Using dynamic sections + Dynamic sections are a simple, resizable visual group to group items. ![](./img/create-dynamic-section.png) @@ -303,6 +354,7 @@ After adding a dynamic section, you can simply drag and drop items into the sect ![](./img/dynamic-section.gif) ### Using categories + Categories are more advanced and complicated than dynamic sections. To create a category, open the add item modal and choose "new category": @@ -329,22 +381,27 @@ You cannot reduce their horizontal width. After completing the onboarding, Homarr provides interactive guided tours to help you discover key features. ### Management tour + When you first visit the management area (`/manage`), Homarr walks you through the main pages: boards, apps, integrations, and users (admin only). Each page highlights the list view and the create action so you know where to find and add things. ### Board tour + When you first open a board, a tour highlights the board header elements: search, edit mode, settings, board switcher, and user menu. ### Tour controls + - Press **Enter** (or click **Next**) to advance through tour steps - Tours auto-start on first visit and do not repeat once completed - Tours are skipped on mobile screens (below `sm` breakpoint) - You can **reset tours** from the user settings page under **Manage > Users > [Your user] > General** ## Next steps + You're done with the basic setup 🔥. Now it's time to organize and customize even more. We highly recommend you to experiment with different options, styles and configurations. See these curated pages for your next steps: + - Create new apps using the [apps management](/docs/management/apps/) - Customize your board or add more boards using the [boards management](/docs/management/boards/) - Integrate software that you use using the [integrations management](/docs/management/integrations/) diff --git a/apps/docs/docs/getting-started/installation/helm.md b/apps/docs/docs/getting-started/installation/helm.md index 8a3903bf1..1f6aa6e9e 100644 --- a/apps/docs/docs/getting-started/installation/helm.md +++ b/apps/docs/docs/getting-started/installation/helm.md @@ -2,9 +2,9 @@ homarr logo -![Version: 8.19.0](https://img.shields.io/badge/Version-8.19.0-informational?style=flat) +![Version: 8.21.0](https://img.shields.io/badge/Version-8.21.0-informational?style=flat) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat) -![AppVersion: v1.60.0](https://img.shields.io/badge/AppVersion-v1.60.0-informational?style=flat) +![AppVersion: v1.69.2](https://img.shields.io/badge/AppVersion-v1.69.2-informational?style=flat) A Helm chart to deploy homarr for Kubernetes @@ -383,6 +383,7 @@ All available values are listed on the [artifacthub](https://artifacthub.io/pack | containerPorts | object | `{"http":{"port":7575,"protocol":"TCP"}}` | containerPorts defines the ports to open on the container. It is a map where each entry specifies: - `port` (int) (required): The port number to expose inside the container. - `protocol` (string) (required): The network protocol (TCP or UDP) used for the port. - `disabled` (bool) : Optional flag to disable this port (defaults to false). Can be overridden via Helm values. By default, this configuration exposes TCP port 7575 with the name `http`. | | database.migrationEnabled | bool | `true` | Database migration configuration. DB_MIGRATIONS_DISABLED Set to `true` to disable database migrations. Migrations are enabled by default (`false`). | | database.type | string | `"sqlite"` | Database type: sqlite, mysql or postgresql | +| env.AUTH_COOKIE_PREFIX | string | `"homarr"` | Prefix used for all authentication cookies. Change if you run another Auth.js/NextAuth app on the same hostname to avoid cookie name collisions. Only letters, numbers, hyphens and underscores. | | env.AUTH_LDAP_BASE | string | `nil` | Base dn of your LDAP server | | env.AUTH_LDAP_BIND_DN | string | `nil` | User used for finding users and groups | | env.AUTH_LDAP_GROUP_CLASS | string | `"groupOfUniqueNames"` | Class used for querying groups | @@ -395,7 +396,6 @@ All available values are listed on the [artifacthub](https://artifacthub.io/pack | env.AUTH_LDAP_USERNAME_FILTER_EXTRA_ARG | string | `nil` | Extra arguments for user search filter (& based) | | env.AUTH_LDAP_USER_MAIL_ATTRIBUTE | string | `"mail"` | Attribute used for mail field | | env.AUTH_LOGOUT_REDIRECT_URL | string | `nil` | URL to redirect to after clicking logging out. | -| env.AUTH_COOKIE_PREFIX | string | `"homarr"` | Prefix used for all authentication cookies. Change if you run another Auth.js/NextAuth app on the same hostname to avoid cookie name collisions. Only letters, numbers, hyphens and underscores. | | env.AUTH_OIDC_AUTO_LOGIN | string | `"false"` | Automatically redirect to OIDC login | | env.AUTH_OIDC_CLIENT_NAME | string | `"OIDC"` | Display name of provider (in login screen) | | env.AUTH_OIDC_GROUPS_ATTRIBUTE | string | `"groups"` | Attribute used for groups (roles) claim | @@ -429,7 +429,7 @@ All available values are listed on the [artifacthub](https://artifacthub.io/pack | httproute.rules[0].matches[0].path.value | string | `"/"` | Path value to match | | image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | | image.repository | string | `"ghcr.io/homarr-labs/homarr"` | Image repository | -| image.tag | string | `"v1.60.0"` | Overrides the image tag whose default is the chart appVersion | +| image.tag | string | `"v1.69.2"` | Overrides the image tag whose default is the chart appVersion | | imagePullSecrets | list | `[]` | Secrets for Docker registry | | ingress.annotations | object | `{}` | Ingress annotations | | ingress.enabled | bool | `false` | Enable ingress | diff --git a/apps/docs/docs/integrations/archiveteam-warrior/index.mdx b/apps/docs/docs/integrations/archiveteam-warrior/index.mdx new file mode 100644 index 000000000..27256aee8 --- /dev/null +++ b/apps/docs/docs/integrations/archiveteam-warrior/index.mdx @@ -0,0 +1,35 @@ +--- +title: "ArchiveTeam Warrior" +description: "ArchiveTeam Warrior is an easy-to-run virtual machine that helps preserve websites by using some of your bandwidth and disk space to download and upload content to ArchiveTeam’s archive." +hide_title: true +--- + +import { IntegrationHeader } from "@site/src/components/integrations/header"; +import { AddingIntegration } from "@site/src/components/integrations/adding"; +import { IntegrationSecrets } from "@site/src/components/integrations/secrets"; +import { archiveTeamWarriorIntegration } from "."; + + + +The ArchiveTeam Warrior integration lets Homarr connect to your ArchiveTeam Warrior instance so you can manage and monitor your archiving tasks directly from your dashboard. + +### Adding the integration + + + +### Secrets + +ArchiveTeam Warrior does not require authentication by default. If your instance is protected with basic authentication, you can provide a username and password. + + diff --git a/apps/docs/docs/integrations/archiveteam-warrior/index.ts b/apps/docs/docs/integrations/archiveteam-warrior/index.ts new file mode 100644 index 000000000..5a18f6f4f --- /dev/null +++ b/apps/docs/docs/integrations/archiveteam-warrior/index.ts @@ -0,0 +1,9 @@ +import { IntegrationDefinition } from "@site/src/types"; + +export const archiveTeamWarriorIntegration: IntegrationDefinition = { + name: "ArchiveTeam Warrior", + description: + "ArchiveTeam Warrior is an easy-to-run virtual machine that helps preserve websites by using some of your bandwidth and disk space to download and upload content to ArchiveTeam’s archive.", + iconUrl: "https://cdn.jsdelivr.net/gh/selfhst/icons/png/archiveteam-warrior.png", + path: "../../integrations/archiveteam-warrior", +}; diff --git a/apps/docs/docs/integrations/bazarr/img/api-key-settings.png b/apps/docs/docs/integrations/bazarr/img/api-key-settings.png new file mode 100644 index 000000000..c3f203b58 Binary files /dev/null and b/apps/docs/docs/integrations/bazarr/img/api-key-settings.png differ diff --git a/apps/docs/docs/integrations/bazarr/index.mdx b/apps/docs/docs/integrations/bazarr/index.mdx new file mode 100644 index 000000000..f9400a45e --- /dev/null +++ b/apps/docs/docs/integrations/bazarr/index.mdx @@ -0,0 +1,54 @@ +--- +title: 'Bazarr' +description: "Bazarr is a companion application to Sonarr and Radarr that manages and downloads subtitles." +hide_title: true +--- + +import { IntegrationHeader } from '@site/src/components/integrations/header'; +import { IntegrationCapabilites } from '@site/src/components/integrations/widgets'; +import { AddingIntegration } from '@site/src/components/integrations/adding'; +import { IntegrationSecrets } from '@site/src/components/integrations/secrets'; +import { bazarrIntegration } from '.'; +import { bazarrWidget } from '@site/docs/widgets/bazarr'; + + + +Bazarr complements Sonarr and Radarr by managing subtitle downloads. The Homarr integration uses your Bazarr API key to fetch missing subtitle counts and health badges. + +### Screenshots + +import apiKeySettings from './img/api-key-settings.png'; +import widgetOverview from '@site/docs/widgets/bazarr/img/widget-overview.png'; + +
+
+

API key location in Bazarr (Settings → General → Security):

+ bazarr-api-key-settings +
+
+

Bazarr widget on a Homarr board:

+ bazarr-widget-on-board +
+
+ +### Widgets & Capabilities + + +### Adding the integration + + +### Secrets +You can find your Bazarr API key under **Settings → General → Security**. + + diff --git a/apps/docs/docs/integrations/bazarr/index.ts b/apps/docs/docs/integrations/bazarr/index.ts new file mode 100644 index 000000000..528f7ae77 --- /dev/null +++ b/apps/docs/docs/integrations/bazarr/index.ts @@ -0,0 +1,9 @@ +import { IntegrationDefinition } from "@site/src/types"; + +export const bazarrIntegration: IntegrationDefinition = { + name: "Bazarr", + description: + "Bazarr is a companion application to Sonarr and Radarr that manages and downloads subtitles for your media library.", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/bazarr.svg", + path: "../../integrations/bazarr", +}; diff --git a/apps/docs/docs/integrations/codeberg/index.mdx b/apps/docs/docs/integrations/codeberg/index.mdx deleted file mode 100644 index 98ca314c2..000000000 --- a/apps/docs/docs/integrations/codeberg/index.mdx +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: 'Codeberg' -description: "Codeberg is a non-profit, community-driven platform for hosting and collaborating on software projects." -hide_title: true ---- - -import { IntegrationHeader } from '@site/src/components/integrations/header'; -import { IntegrationCapabilites } from '@site/src/components/integrations/widgets'; -import { AddingIntegration } from '@site/src/components/integrations/adding'; -import { IntegrationSecrets } from '@site/src/components/integrations/secrets'; -import { codebergIntegration } from '.'; -import { releasesWidget } from '@site/docs/widgets/releases'; - - - - -### Widgets & Capabilities - - -### Adding the integration - - -### Secrets - \ No newline at end of file diff --git a/apps/docs/docs/integrations/codeberg/index.ts b/apps/docs/docs/integrations/codeberg/index.ts deleted file mode 100644 index f2a43ad48..000000000 --- a/apps/docs/docs/integrations/codeberg/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IntegrationDefinition } from "@site/src/types"; - -export const codebergIntegration: IntegrationDefinition = { - name: "Codeberg", - description: - "Codeberg is a non-profit, community-driven platform for hosting and collaborating on software projects.", - iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/codeberg.svg", - path: "../../integrations/codeberg", -}; diff --git a/apps/docs/docs/integrations/docker-hub/index.mdx b/apps/docs/docs/integrations/docker-hub/index.mdx deleted file mode 100644 index a49d03047..000000000 --- a/apps/docs/docs/integrations/docker-hub/index.mdx +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: 'Docker Hub' -description: "Docker Hub is a cloud-based registry service for sharing and managing Docker images." -hide_title: true ---- - -import { IntegrationHeader } from '@site/src/components/integrations/header'; -import { IntegrationCapabilites } from '@site/src/components/integrations/widgets'; -import { AddingIntegration } from '@site/src/components/integrations/adding'; -import { IntegrationSecrets } from '@site/src/components/integrations/secrets'; -import { dockerHubIntegration } from '.'; -import { releasesWidget } from '@site/docs/widgets/releases'; - - - - -### Widgets & Capabilities - - -### Adding the integration - - -### Secrets - \ No newline at end of file diff --git a/apps/docs/docs/integrations/docker-hub/index.ts b/apps/docs/docs/integrations/docker-hub/index.ts deleted file mode 100644 index 1a49feeae..000000000 --- a/apps/docs/docs/integrations/docker-hub/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { IntegrationDefinition } from "@site/src/types"; - -export const dockerHubIntegration: IntegrationDefinition = { - name: "Docker Hub", - description: "Docker Hub is a cloud-based registry service for sharing and managing Docker images.", - iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/docker.svg", - path: "../../integrations/docker-hub", -}; diff --git a/apps/docs/docs/integrations/github-containerregistry/index.mdx b/apps/docs/docs/integrations/github-containerregistry/index.mdx deleted file mode 100644 index f4cb5b310..000000000 --- a/apps/docs/docs/integrations/github-containerregistry/index.mdx +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: 'GitHub Container Registry' -description: "GitHub Container Registry is a platform for hosting and managing packages." -hide_title: true ---- - -import { IntegrationHeader } from '@site/src/components/integrations/header'; -import { IntegrationCapabilites } from '@site/src/components/integrations/widgets'; -import { AddingIntegration } from '@site/src/components/integrations/adding'; -import { IntegrationSecrets } from '@site/src/components/integrations/secrets'; -import { gitHubContainerRegistryIntegration } from '.'; -import { releasesWidget } from '@site/docs/widgets/releases'; - - - - - -### Widgets & Capabilities - - -### Adding the integration - - -### Secrets - \ No newline at end of file diff --git a/apps/docs/docs/integrations/github-containerregistry/index.ts b/apps/docs/docs/integrations/github-containerregistry/index.ts deleted file mode 100644 index b88f426d3..000000000 --- a/apps/docs/docs/integrations/github-containerregistry/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { IntegrationDefinition } from "@site/src/types"; - -export const gitHubContainerRegistryIntegration: IntegrationDefinition = { - name: "GitHub Container Registry", - description: "GitHub Container Registry is a platform for hosting and managing packages.", - iconUrl: { - light: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/github.svg", - dark: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/github-light.svg", - }, - path: "../../integrations/github-packages", -}; diff --git a/apps/docs/docs/integrations/github/index.mdx b/apps/docs/docs/integrations/github/index.mdx deleted file mode 100644 index 39c9777b3..000000000 --- a/apps/docs/docs/integrations/github/index.mdx +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: 'GitHub' -description: "GitHub is a web-based platform for version control and collaboration." -hide_title: true ---- - -import { IntegrationHeader } from '@site/src/components/integrations/header'; -import { IntegrationCapabilites } from '@site/src/components/integrations/widgets'; -import { AddingIntegration } from '@site/src/components/integrations/adding'; -import { IntegrationSecrets } from '@site/src/components/integrations/secrets'; -import { githubIntegration } from '.'; -import { releasesWidget } from '@site/docs/widgets/releases'; - - - - -### Widgets & Capabilities - - -### Adding the integration - - -### Secrets - \ No newline at end of file diff --git a/apps/docs/docs/integrations/github/index.ts b/apps/docs/docs/integrations/github/index.ts deleted file mode 100644 index 7bb9b6b5d..000000000 --- a/apps/docs/docs/integrations/github/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { IntegrationDefinition } from "@site/src/types"; - -export const githubIntegration: IntegrationDefinition = { - name: "GitHub", - description: "GitHub is a web-based platform for version control and collaboration.", - iconUrl: { - light: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/github.svg", - dark: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/github-light.svg", - }, - path: "../../integrations/github", -}; diff --git a/apps/docs/docs/integrations/gitlab/index.mdx b/apps/docs/docs/integrations/gitlab/index.mdx deleted file mode 100644 index edc77e1cd..000000000 --- a/apps/docs/docs/integrations/gitlab/index.mdx +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: 'GitLab' -description: "GitLab is a web-based DevOps lifecycle tool that provides a Git repository manager." -hide_title: true ---- - -import { IntegrationHeader } from '@site/src/components/integrations/header'; -import { IntegrationCapabilites } from '@site/src/components/integrations/widgets'; -import { AddingIntegration } from '@site/src/components/integrations/adding'; -import { IntegrationSecrets } from '@site/src/components/integrations/secrets'; -import { gitlabIntegration } from '.'; -import { releasesWidget } from '@site/docs/widgets/releases'; - - - - -### Widgets & Capabilities - - -### Adding the integration - - -### Secrets - \ No newline at end of file diff --git a/apps/docs/docs/integrations/gitlab/index.ts b/apps/docs/docs/integrations/gitlab/index.ts deleted file mode 100644 index c252cd6ff..000000000 --- a/apps/docs/docs/integrations/gitlab/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { IntegrationDefinition } from "@site/src/types"; - -export const gitlabIntegration: IntegrationDefinition = { - name: "GitLab", - description: "GitLab is a web-based DevOps lifecycle tool that provides a Git repository manager.", - iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/gitlab.svg", - path: "../../integrations/gitlab", -}; diff --git a/apps/docs/docs/integrations/immich/index.mdx b/apps/docs/docs/integrations/immich/index.mdx index 4826633e2..ed88466ce 100644 --- a/apps/docs/docs/integrations/immich/index.mdx +++ b/apps/docs/docs/integrations/immich/index.mdx @@ -39,13 +39,26 @@ import { immichServerStatsWidget } from '../../widgets/immich-server-stats'; 'Scroll down to "API Keys"', 'Click "New API Key"', 'Enter a name, e.g. "Homarr"', - ( - Select the following permissions:{" "} - album.read,{" "} - user.read,{" "} - server.statistics,{" "} - asset.download - ), +( + + + + Select the following permissions:{" "} + album.statistics,{" "} + album.read,{" "} + album.create,{" "} + user.read,{" "} + user.update,{" "} + asset.read,{" "} + asset.download,{" "} + asset.statistics,{" "} + server.statistics,{" "} + server.about. +
+ Additional permissions may be required depending on your Immich version. If you encounter authorization errors, grant any missing permissions requested by the server. +
+ +), 'Create the integration in Homarr and copy the API key to there' ] -}]} /> \ No newline at end of file +}]} /> diff --git a/apps/docs/docs/integrations/linux-server-io/index.mdx b/apps/docs/docs/integrations/linux-server-io/index.mdx deleted file mode 100644 index 426dc7f68..000000000 --- a/apps/docs/docs/integrations/linux-server-io/index.mdx +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: 'LinuxServer.io' -description: "LinuxServer.io is a platform for managing and deploying containerized applications." -hide_title: true ---- - -import { IntegrationHeader } from '@site/src/components/integrations/header'; -import { IntegrationCapabilites } from '@site/src/components/integrations/widgets'; -import { AddingIntegration } from '@site/src/components/integrations/adding'; -import { IntegrationSecrets } from '@site/src/components/integrations/secrets'; -import { linuxServerIoIntegration } from '.'; -import { releasesWidget } from '@site/docs/widgets/releases'; - - - - -### Widgets & Capabilities - - -### Adding the integration - - -### Secrets - \ No newline at end of file diff --git a/apps/docs/docs/integrations/linux-server-io/index.ts b/apps/docs/docs/integrations/linux-server-io/index.ts deleted file mode 100644 index 82bc0fac6..000000000 --- a/apps/docs/docs/integrations/linux-server-io/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { IntegrationDefinition } from "@site/src/types"; - -export const linuxServerIoIntegration: IntegrationDefinition = { - name: "LinuxServer.io", - description: "LinuxServer.io is a platform for managing and deploying containerized applications.", - iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/linuxserver-io.svg", - path: "../../integrations/linux-server-io", -}; diff --git a/apps/docs/docs/integrations/npm/index.mdx b/apps/docs/docs/integrations/npm/index.mdx deleted file mode 100644 index c4d21135e..000000000 --- a/apps/docs/docs/integrations/npm/index.mdx +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: 'NPM' -description: "NPM is a package manager for the JavaScript programming language, primarily used for managing Node.js packages." -hide_title: true ---- - -import { IntegrationHeader } from '@site/src/components/integrations/header'; -import { IntegrationCapabilites } from '@site/src/components/integrations/widgets'; -import { AddingIntegration } from '@site/src/components/integrations/adding'; -import { IntegrationSecrets } from '@site/src/components/integrations/secrets'; -import { npmIntegration } from '.'; -import { releasesWidget } from '@site/docs/widgets/releases'; - - - - -### Widgets & Capabilities - - -### Adding the integration - - -### Secrets - \ No newline at end of file diff --git a/apps/docs/docs/integrations/npm/index.ts b/apps/docs/docs/integrations/npm/index.ts deleted file mode 100644 index 34fbafb5c..000000000 --- a/apps/docs/docs/integrations/npm/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IntegrationDefinition } from "@site/src/types"; - -export const npmIntegration: IntegrationDefinition = { - name: "NPM", - description: - "NPM is a package manager for the JavaScript programming language, primarily used for managing Node.js packages.", - iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/npm.svg", - path: "../../integrations/npm", -}; diff --git a/apps/docs/docs/integrations/patchmon/index.mdx b/apps/docs/docs/integrations/patchmon/index.mdx new file mode 100644 index 000000000..95b827df0 --- /dev/null +++ b/apps/docs/docs/integrations/patchmon/index.mdx @@ -0,0 +1,43 @@ +--- +title: 'PatchMon' +description: "PatchMon is a patch management and monitoring solution for Linux hosts." +hide_title: true +--- + +import { IntegrationHeader } from '@site/src/components/integrations/header'; +import { IntegrationCapabilites } from '@site/src/components/integrations/widgets'; +import { AddingIntegration } from '@site/src/components/integrations/adding'; +import { IntegrationSecrets } from '@site/src/components/integrations/secrets'; +import { patchmonIntegration } from '.'; +import { patchmonWidget } from '@site/docs/widgets/patchmon'; + + + + +PatchMon provides host and package update statistics including total hosts, hosts needing updates, and security update counts. + +The PatchMon widget requires users to be **signed in to Homarr**; see the [PatchMon widget documentation](/docs/widgets/patchmon#authentication) for details on authentication and public boards. + +### Widgets & Capabilities + + +### Adding the integration + + +### Secrets + diff --git a/apps/docs/docs/integrations/patchmon/index.ts b/apps/docs/docs/integrations/patchmon/index.ts new file mode 100644 index 000000000..912c09f16 --- /dev/null +++ b/apps/docs/docs/integrations/patchmon/index.ts @@ -0,0 +1,9 @@ +import { IntegrationDefinition } from "@site/src/types"; + +export const patchmonIntegration: IntegrationDefinition = { + name: "PatchMon", + description: + "PatchMon is a patch management and monitoring solution for Linux hosts, tracking package updates and security patches across your infrastructure.", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/patchmon.svg", + path: "../../integrations/patchmon", +}; diff --git a/apps/docs/docs/integrations/quay/index.mdx b/apps/docs/docs/integrations/quay/index.mdx deleted file mode 100644 index 9033f81cf..000000000 --- a/apps/docs/docs/integrations/quay/index.mdx +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: 'Quay' -description: "Quay is a container registry for storing and managing container images." -hide_title: true ---- - -import { IntegrationHeader } from '@site/src/components/integrations/header'; -import { IntegrationCapabilites } from '@site/src/components/integrations/widgets'; -import { AddingIntegration } from '@site/src/components/integrations/adding'; -import { IntegrationSecrets } from '@site/src/components/integrations/secrets'; -import { quayIntegration } from '.'; -import { releasesWidget } from '@site/docs/widgets/releases'; - - - - -### Widgets & Capabilities - - -### Adding the integration - - -### Secrets - \ No newline at end of file diff --git a/apps/docs/docs/integrations/quay/index.ts b/apps/docs/docs/integrations/quay/index.ts deleted file mode 100644 index 315732bff..000000000 --- a/apps/docs/docs/integrations/quay/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { IntegrationDefinition } from "@site/src/types"; - -export const quayIntegration: IntegrationDefinition = { - name: "Quay", - description: "Quay is a container registry for storing and managing container images.", - iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/quay.png", - path: "../../integrations/quay", -}; diff --git a/apps/docs/docs/integrations/search-ch/index.mdx b/apps/docs/docs/integrations/search-ch/index.mdx deleted file mode 100644 index 6286d5cca..000000000 --- a/apps/docs/docs/integrations/search-ch/index.mdx +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: 'Search.ch' -description: "Search.ch supports searching for timetables in Switzerland." -hide_title: true ---- - -import { IntegrationHeader } from '@site/src/components/integrations/header'; -import { IntegrationCapabilites } from '@site/src/components/integrations/widgets'; -import { AddingIntegration } from '@site/src/components/integrations/adding'; -import { IntegrationSecrets } from '@site/src/components/integrations/secrets'; -import { IconDownload } from "@tabler/icons-react"; -import { searchChIntegration } from '.'; -import Alert from '@theme/Admonition'; -import { timetableWidget } from '@site/docs/widgets/timetable'; - - - - -### Widgets & Capabilities - - -### Adding the integration - \ No newline at end of file diff --git a/apps/docs/docs/integrations/search-ch/index.ts b/apps/docs/docs/integrations/search-ch/index.ts deleted file mode 100644 index 6c5819d67..000000000 --- a/apps/docs/docs/integrations/search-ch/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { IntegrationDefinition } from "@site/src/types"; -export const searchChIntegration: IntegrationDefinition = { - name: "Search.ch", - description: "Search.ch supports searching for timetables in Switzerland.", - iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/search-ch.png", - path: "../../integrations/search-ch", -}; diff --git a/apps/docs/docs/integrations/synology/index.mdx b/apps/docs/docs/integrations/synology/index.mdx new file mode 100644 index 000000000..4d836513e --- /dev/null +++ b/apps/docs/docs/integrations/synology/index.mdx @@ -0,0 +1,96 @@ +--- +title: "Synology DiskStation" +description: "Monitor CPU, memory, storage volumes, and system health from Synology DSM." +hide_title: true +--- + +import { IntegrationHeader } from "@site/src/components/integrations/header"; +import { IntegrationCapabilites } from "@site/src/components/integrations/widgets"; +import { AddingIntegration } from "@site/src/components/integrations/adding"; +import { IntegrationSecrets } from "@site/src/components/integrations/secrets"; +import { synologyIntegration } from "."; +import { systemResourcesWidget } from "@site/docs/widgets/system-resources"; +import { systemDisksWidget } from "@site/docs/widgets/system-disks"; +import { healthMonitoringWidget } from "@site/docs/widgets/health-monitoring"; +import Alert from "@theme/Admonition"; + + + +### Widgets & Capabilities + + + +### Supported metrics + +| Widget section | DSM API source | May be unavailable | +| ---------------------------- | ----------------------------------------------------- | ------------------------------------------------------- | +| CPU ring / chart | `SYNO.Core.System.Utilization` | — | +| Memory ring / chart | `SYNO.Core.System.Utilization` | — | +| CPU temperature ring | `SYNO.Core.System` system info | Models without a temperature sensor | +| Network charts | `SYNO.Core.System.Utilization` network totals | Some DSM builds or permission configurations | +| Load average (info modal) | `SYNO.Core.System.Utilization` CPU load fields | — | +| Storage volume usage | `SYNO.Core.System` storage info / Storage Manager API | — | +| SMART status and temperature | Storage Manager and SMART APIs | Disk-to-volume mapping may be incomplete on some setups | +| Reboot / update indicator | `SYNO.Core.System.Status` / upgrade APIs | Permission-dependent | +| GPU | Not available | Typical DiskStation models | + +### Adding the integration + + + + + Enter the HTTP or HTTPS URL of your Synology DSM web interface, for example http://diskstation.local:5000{" "} + or https://diskstation.local:5001. If DSM uses a self-signed certificate, add it under{" "} + Management > Tools > Certificates so Homarr can connect securely. + + +### Secrets + + + + Homarr reads system, storage, and utilization data from APIs that require membership in the default + Administrators group. + + + Accounts with 2-step verification enabled cannot authenticate with this integration. Use a dedicated service + account without 2FA. + + + If login fails during testing, temporarily disable automatic blocking for invalid logins under Control Panel + > Security > Protection, then re-enable it after Homarr connects successfully. + + + ), + }, + ]} +/> + +### Storage volume selection + +Health Monitoring and System Disks widgets can filter Synology storage volumes when **every** selected integration is a Synology DiskStation. Leave **Visible storage volumes** empty to show all volumes, or pick specific volumes from the list. When multiple Synology integrations are connected, volumes are labeled with the integration name. + + + The volume filter is hidden when the widget also uses non-Synology integrations such as TrueNAS or Unraid. + + + + When no volume filter is configured, the first volume returned by DSM is not guaranteed to be `volume_1`. + diff --git a/apps/docs/docs/integrations/synology/index.ts b/apps/docs/docs/integrations/synology/index.ts new file mode 100644 index 000000000..6ccb2aa9e --- /dev/null +++ b/apps/docs/docs/integrations/synology/index.ts @@ -0,0 +1,8 @@ +import { IntegrationDefinition } from "@site/src/types"; + +export const synologyIntegration: IntegrationDefinition = { + name: "Synology DiskStation", + description: "Monitor CPU, memory, storage volumes, and system health from Synology DSM.", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/synology.svg", + path: "../../integrations/synology", +}; diff --git a/apps/docs/docs/integrations/traefik/index.mdx b/apps/docs/docs/integrations/traefik/index.mdx new file mode 100644 index 000000000..819ede6c6 --- /dev/null +++ b/apps/docs/docs/integrations/traefik/index.mdx @@ -0,0 +1,57 @@ +--- +title: "Traefik" +description: "Cloud-native reverse proxy and load balancer" +hide_title: true +--- + +import { IntegrationHeader } from "@site/src/components/integrations/header"; +import { IntegrationCapabilites } from "@site/src/components/integrations/widgets"; +import { AddingIntegration } from "@site/src/components/integrations/adding"; +import { IntegrationSecrets } from "@site/src/components/integrations/secrets"; +import { traefikIntegration } from "."; +import { traefikWidget } from "../../widgets/traefik"; + + + +### Widgets & Capabilities + + + +### Adding the integration + + + +:::info URL configuration +Use the URL where Traefik exposes its dashboard/API, for example `http://traefik:8080` or `https://traefik.example.com`. Homarr calls Traefik endpoints under `/api` automatically. +::: + +:::warning API requirement +Traefik must have the API enabled. For local or internal setups this is commonly exposed through `api.insecure=true`; for production setups, expose the `api@internal` service through an authenticated router. +::: + +### Secrets + + diff --git a/apps/docs/docs/integrations/traefik/index.ts b/apps/docs/docs/integrations/traefik/index.ts new file mode 100644 index 000000000..6e7db80a5 --- /dev/null +++ b/apps/docs/docs/integrations/traefik/index.ts @@ -0,0 +1,8 @@ +import { IntegrationDefinition } from "@site/src/types"; + +export const traefikIntegration: IntegrationDefinition = { + name: "Traefik", + description: "Cloud-native reverse proxy and load balancer", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/traefik.svg", + path: "../../integrations/traefik", +}; diff --git a/apps/docs/docs/management/api/index.mdx b/apps/docs/docs/management/api/index.mdx index 177ec8c9c..2c5bd42e2 100644 --- a/apps/docs/docs/management/api/index.mdx +++ b/apps/docs/docs/management/api/index.mdx @@ -70,4 +70,16 @@ API key authentication enables programmatic access for: - Automation scripts and CI/CD pipelines - Third-party integrations - Custom dashboards and monitoring tools -- Container orchestration systems that need to sync app definitions \ No newline at end of file +- Container orchestration systems that need to sync app definitions + +## Board Automation + +The OpenAPI and MCP APIs include board management endpoints for common automation workflows: + +- Create, duplicate, rename and delete boards +- Change board visibility +- Update board visual and behavior settings +- Set desktop and mobile home boards for the current user +- Configure global board defaults such as public home boards and status behavior + +Use the interactive API reference to inspect the exact request body for each endpoint under the `boards` and `settings` tags. diff --git a/apps/docs/docs/management/mcp.mdx b/apps/docs/docs/management/mcp.mdx index 5317ecce2..7c79d9a8d 100644 --- a/apps/docs/docs/management/mcp.mdx +++ b/apps/docs/docs/management/mcp.mdx @@ -141,6 +141,7 @@ Homarr exposes 50+ tools organized by namespace. Each tool is either a **query** | `dnsHole` | DNS hole summary, toggle blocking | GET + POST | | `downloads` | Active downloads, pause/resume/delete | GET + POST | | `healthMonitoring` | Resource usage stats | GET | +| `patchmon` | Host patch and security update statistics | GET | | `mediaRequests` | Approve/decline media requests | GET + POST | | `mediaServer` | Media server sessions and stats | GET | | `smartHome` | Execute automations, device states | GET + POST | diff --git a/apps/docs/docs/widgets/bazarr/img/widget-overview.png b/apps/docs/docs/widgets/bazarr/img/widget-overview.png new file mode 100644 index 000000000..d5af06b69 Binary files /dev/null and b/apps/docs/docs/widgets/bazarr/img/widget-overview.png differ diff --git a/apps/docs/docs/widgets/bazarr/index.mdx b/apps/docs/docs/widgets/bazarr/index.mdx new file mode 100644 index 000000000..e262cfa75 --- /dev/null +++ b/apps/docs/docs/widgets/bazarr/index.mdx @@ -0,0 +1,41 @@ +--- +title: 'Bazarr' +description: 'Displays missing subtitle counts and health indicators from your Bazarr instance.' +hide_title: true +--- + +import { WidgetHeader } from '@site/src/components/widgets/header'; +import { AddingWidget } from '@site/src/components/widgets/adding'; +import { WidgetConfig } from '@site/src/components/widgets/configuration'; +import { WidgetIntegrations } from '@site/src/components/widgets/integrations'; +import { bazarrWidget } from '.'; +import { bazarrIntegration } from '@site/docs/integrations/bazarr'; + + + +This widget shows subtitle status from your Bazarr instance, including missing episode and movie subtitles, provider issues, and health warnings. + +### Screenshots + +import widgetOverview from './img/widget-overview.png'; + +
+ bazarr-widget-overview +
+ +### Supported Integrations + + + +### Adding the widget + + + +### Configuration + diff --git a/apps/docs/docs/widgets/bazarr/index.ts b/apps/docs/docs/widgets/bazarr/index.ts new file mode 100644 index 000000000..8b5c27ef8 --- /dev/null +++ b/apps/docs/docs/widgets/bazarr/index.ts @@ -0,0 +1,37 @@ +import { WidgetDefinition } from "@site/src/types"; +import { IconBadgeCc } from "@tabler/icons-react"; + +export const bazarrWidget: WidgetDefinition = { + icon: IconBadgeCc, + name: "Bazarr", + description: "Displays missing subtitle counts and health indicators from your Bazarr instance.", + path: "../../widgets/bazarr", + configuration: { + items: [ + { + name: "Show missing episodes", + description: "Displays the number of episodes with missing subtitles", + values: { type: "boolean" }, + defaultValue: "yes", + }, + { + name: "Show missing movies", + description: "Displays the number of movies with missing subtitles", + values: { type: "boolean" }, + defaultValue: "yes", + }, + { + name: "Show provider issues", + description: "Displays the number of subtitle providers with issues", + values: { type: "boolean" }, + defaultValue: "yes", + }, + { + name: "Show health issues", + description: "Displays the number of Bazarr health issues", + values: { type: "boolean" }, + defaultValue: "yes", + }, + ], + }, +}; diff --git a/apps/docs/docs/widgets/beszel-system-grid/index.mdx b/apps/docs/docs/widgets/beszel-system-grid/index.mdx index de3578aeb..e3b17623b 100644 --- a/apps/docs/docs/widgets/beszel-system-grid/index.mdx +++ b/apps/docs/docs/widgets/beszel-system-grid/index.mdx @@ -18,7 +18,9 @@ import beszelSystemGrid from './img/beszel-system-grid.png'; categories={['System Monitoring']} /> -This widget displays all your Beszel-monitored systems as a responsive card grid. Each card shows real-time metrics like CPU, memory, disk, network, temperature, and more. Cards adapt their layout based on available space. +This widget displays all your Beszel-monitored systems as a responsive card grid. Each card shows real-time metrics like CPU, memory, disk, network, temperature, and more. Cards adapt their layout based on available space. If Beszel reports multiple filesystems, the disk bar exposes a hover breakdown for the root disk and each extra mount. + +Select a system card to open the complete statistics view, including system, filesystem, Docker, historical, and realtime charts. ### Screenshots diff --git a/apps/docs/docs/widgets/beszel-system-stats/index.mdx b/apps/docs/docs/widgets/beszel-system-stats/index.mdx index 8efa3fb46..1e9c896b9 100644 --- a/apps/docs/docs/widgets/beszel-system-stats/index.mdx +++ b/apps/docs/docs/widgets/beszel-system-stats/index.mdx @@ -1,24 +1,21 @@ --- -title: 'Beszel System Stats' -description: 'Time-series charts for CPU, memory, disk, network, and Docker container metrics from Beszel.' +title: "Beszel System Stats" +description: "Time-series charts for CPU, memory, disk, network, and Docker container metrics from Beszel." hide_title: true --- -import { WidgetHeader } from '@site/src/components/widgets/header'; -import { AddingWidget } from '@site/src/components/widgets/adding'; -import { WidgetConfig } from '@site/src/components/widgets/configuration'; -import { WidgetIntegrations } from '@site/src/components/widgets/integrations'; -import { beszelSystemStatsWidget } from '.'; -import { beszelIntegration } from '@site/docs/integrations/beszel'; +import { WidgetHeader } from "@site/src/components/widgets/header"; +import { AddingWidget } from "@site/src/components/widgets/adding"; +import { WidgetConfig } from "@site/src/components/widgets/configuration"; +import { WidgetIntegrations } from "@site/src/components/widgets/integrations"; +import { beszelSystemStatsWidget } from "."; +import { beszelIntegration } from "@site/docs/integrations/beszel"; -import beszelSystemStats from './img/beszel-system-stats.png'; +import beszelSystemStats from "./img/beszel-system-stats.png"; - + -This widget displays time-series charts for a selected Beszel system, including CPU, memory, disk, network, and Docker container metrics. Data is cached and refreshed periodically. You can select a time period ranging from 1 hour to 30 days. +This widget displays time-series charts for a selected Beszel system, including CPU, memory, disk, network, and Docker container metrics. The Disk Usage chart combines the root disk and every extra filesystem reported by Beszel. You can use the realtime Live view or select a historical period ranging from 1 hour to 30 days. If the live stream drops, the widget now shows the connection failure state immediately and can switch back to historical data. ### Screenshots @@ -26,14 +23,19 @@ This widget displays time-series charts for a selected Beszel system, including ### Supported Integrations - + ### Adding the widget ### Configuration + diff --git a/apps/docs/docs/widgets/beszel-system-table/index.mdx b/apps/docs/docs/widgets/beszel-system-table/index.mdx index dfab7a977..df68fcc1b 100644 --- a/apps/docs/docs/widgets/beszel-system-table/index.mdx +++ b/apps/docs/docs/widgets/beszel-system-table/index.mdx @@ -17,7 +17,9 @@ import { beszelIntegration } from '@site/docs/integrations/beszel'; categories={['System Monitoring']} /> -This widget displays all your Beszel-monitored systems in a sortable data table. Each row shows system metrics like CPU, memory, disk, network, temperature, and more. Columns can be toggled on or off, and the table supports sorting by any column. +This widget displays all your Beszel-monitored systems in a sortable data table. Each row shows system metrics like CPU, memory, disk, network, temperature, and more. Columns can be toggled on or off, and the table supports sorting by any column. When Beszel reports extra filesystems, the disk column adds a hover breakdown for each mount alongside the root disk. + +Select a system row to open the complete statistics view, including system, filesystem, Docker, historical, and realtime charts. ### Supported Integrations diff --git a/apps/docs/docs/widgets/calendar/index.mdx b/apps/docs/docs/widgets/calendar/index.mdx index 60095aabe..5db4e9b30 100644 --- a/apps/docs/docs/widgets/calendar/index.mdx +++ b/apps/docs/docs/widgets/calendar/index.mdx @@ -63,7 +63,9 @@ import withoutIntegrations from './img/without-integrations.png'; ### Interactions -- View calendar events by clicking on the marked dates. +- Hover over a marked date to preview its events in a popover. +- Click on a marked date to pin the popover open (it will stay visible even after moving the mouse away). +- Click the date again or click outside to close the pinned popover. ### Adding the widget diff --git a/apps/docs/docs/widgets/health-monitoring/index.mdx b/apps/docs/docs/widgets/health-monitoring/index.mdx index 9e41a5fcc..603207451 100644 --- a/apps/docs/docs/widgets/health-monitoring/index.mdx +++ b/apps/docs/docs/widgets/health-monitoring/index.mdx @@ -1,61 +1,71 @@ --- -title: 'System Health Monitoring' -description: 'Displays information showing the health and status of your system(s).' +title: "System Health Monitoring" +description: "Displays information showing the health and status of your system(s)." hide_title: true --- -import { WidgetHeader } from '@site/src/components/widgets/header'; -import { AddingWidget } from '@site/src/components/widgets/adding'; -import { WidgetConfig } from '@site/src/components/widgets/configuration'; -import { WidgetIntegrations } from '@site/src/components/widgets/integrations'; +import { WidgetHeader } from "@site/src/components/widgets/header"; +import { AddingWidget } from "@site/src/components/widgets/adding"; +import { WidgetConfig } from "@site/src/components/widgets/configuration"; +import { WidgetIntegrations } from "@site/src/components/widgets/integrations"; import { IconAd } from "@tabler/icons-react"; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; -import { healthMonitoringWidget } from '.'; -import { dashDotIntegration } from '@site/docs/integrations/dash-dot'; -import { openMediaVaultIntegration } from '@site/docs/integrations/open-media-vault'; -import { proxmoxIntegration } from '@site/docs/integrations/proxmox'; -import { glancesIntegration } from '@site/docs/integrations/glances'; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import { healthMonitoringWidget } from "."; +import { dashDotIntegration } from "@site/docs/integrations/dash-dot"; +import { openMediaVaultIntegration } from "@site/docs/integrations/open-media-vault"; +import { proxmoxIntegration } from "@site/docs/integrations/proxmox"; +import { glancesIntegration } from "@site/docs/integrations/glances"; - - + This widget displays information about the health and status of your system(s), including CPU, memory, disk usage, and more. ### Screenshots -import systemView from './img/system-view.png'; -import clusterView from './img/cluster-view.png'; -import { truenasIntegration } from '@site/docs/integrations/truenas'; +import systemView from "./img/system-view.png"; +import clusterView from "./img/cluster-view.png"; +import { truenasIntegration } from "@site/docs/integrations/truenas"; +import { synologyIntegration } from "@site/docs/integrations/synology"; health-monitoring system-view health-monitoring cluster-view ### Supported Integrations - + ### Adding the widget ### Configuration + diff --git a/apps/docs/docs/widgets/health-monitoring/index.ts b/apps/docs/docs/widgets/health-monitoring/index.ts index 2a3cc6a58..6e28a9026 100644 --- a/apps/docs/docs/widgets/health-monitoring/index.ts +++ b/apps/docs/docs/widgets/health-monitoring/index.ts @@ -38,6 +38,13 @@ export const healthMonitoringWidget: WidgetDefinition = { values: { type: "boolean" }, defaultValue: "yes", }, + { + name: "Visible storage volumes", + description: + "Only available when all selected integrations are Synology. Leave empty to show all detected storage volumes.", + values: "List of storage volumes", + defaultValue: "Empty list", + }, { name: "Default tab", description: "Tab that should be visible by default. Only used when multiple integrations are available.", diff --git a/apps/docs/docs/widgets/immich-album-carousel/index.ts b/apps/docs/docs/widgets/immich-album-carousel/index.ts index 327314c3d..f6c4b224c 100644 --- a/apps/docs/docs/widgets/immich-album-carousel/index.ts +++ b/apps/docs/docs/widgets/immich-album-carousel/index.ts @@ -9,9 +9,8 @@ export const immichAlbumCarouselWidget: WidgetDefinition = { configuration: { items: [ { - name: "Album ID", - description: - "This is a unique ID which refers to your Immich album. You can find this id by opening an album and copying the unique string from the URL, for example 'a21aca09-9f05-47dc-b1a1-7b53c547226e'", + name: "Album", + description: "Select the Immich album to show in the slideshow.", values: { type: "string" }, defaultValue: "", }, @@ -25,7 +24,13 @@ export const immichAlbumCarouselWidget: WidgetDefinition = { name: "Show photo info", description: "Display date of the picture and the current index at the bottom of the slideshow", values: { type: "boolean" }, - defaultValue: "yes", + defaultValue: "no", + }, + { + name: "Randomize photos", + description: "Show photos in a random order instead of always starting with the same album order.", + values: { type: "boolean" }, + defaultValue: "no", }, ], }, diff --git a/apps/docs/docs/widgets/media-missing/img/compact.png b/apps/docs/docs/widgets/media-missing/img/compact.png new file mode 100644 index 000000000..a33396ed6 Binary files /dev/null and b/apps/docs/docs/widgets/media-missing/img/compact.png differ diff --git a/apps/docs/docs/widgets/media-missing/img/missing.png b/apps/docs/docs/widgets/media-missing/img/missing.png new file mode 100644 index 000000000..25d4a47cd Binary files /dev/null and b/apps/docs/docs/widgets/media-missing/img/missing.png differ diff --git a/apps/docs/docs/widgets/media-missing/img/queued.png b/apps/docs/docs/widgets/media-missing/img/queued.png new file mode 100644 index 000000000..299f457e5 Binary files /dev/null and b/apps/docs/docs/widgets/media-missing/img/queued.png differ diff --git a/apps/docs/docs/widgets/media-missing/index.mdx b/apps/docs/docs/widgets/media-missing/index.mdx new file mode 100644 index 000000000..648bd4190 --- /dev/null +++ b/apps/docs/docs/widgets/media-missing/index.mdx @@ -0,0 +1,65 @@ +--- +title: 'Media Missing & Queue' +description: 'Track missing media and active downloads from Radarr and Sonarr' +hide_title: true +--- + +import { WidgetHeader } from '@site/src/components/widgets/header'; +import { AddingWidget } from '@site/src/components/widgets/adding'; +import { WidgetConfig } from '@site/src/components/widgets/configuration'; +import { WidgetIntegrations } from '@site/src/components/widgets/integrations'; +import { mediaMissingWidget } from '.'; +import { radarrIntegration } from '@site/docs/integrations/radarr'; +import { sonarrIntegration } from '@site/docs/integrations/sonarr'; + + + +Monitor which movies and episodes are missing from your library, and track what is currently being downloaded. The widget pulls data from Radarr and Sonarr and groups results by type in two tabs. + +### Screenshots + +import widgetMissing from './img/missing.png'; +import widgetQueued from './img/queued.png'; +import widgetCompact from './img/compact.png'; + +
+ media-missing widget showing missing movies and episodes in a grid + media-missing tab showing download queue with progress bars + media-missing widget in a compact wide-and-short size +
+ +### Supported Integrations + + + +### Tabs + +| Tab | Description | +|---|---| +| **Missing** | Lists movies (Radarr) and episodes (Sonarr) that are monitored but not yet available in your library. | +| **Queued** | Shows items currently being downloaded with a progress bar and estimated time remaining. | + +### Sizing + +The widget adapts to its container size, so it looks good at any grid size: + +- **Wide** — fits as many columns of cards as the width allows, filling larger widgets instead of stretching a single column. +- **Short** — packs items into more, tighter columns so they stay visible in the limited height. +- **Narrow (1×N)** — tab labels collapse to counts only and posters scale down. + +Each item shows its poster art (with the artwork bleeding softly behind the card), a type badge, and — on the queue — an animated progress bar with the estimated time remaining. + +### Adding the widget + + + +### Configuration + + diff --git a/apps/docs/docs/widgets/media-missing/index.ts b/apps/docs/docs/widgets/media-missing/index.ts new file mode 100644 index 000000000..a46a19395 --- /dev/null +++ b/apps/docs/docs/widgets/media-missing/index.ts @@ -0,0 +1,31 @@ +import { WidgetDefinition } from "@site/src/types"; +import { IconMovie } from "@tabler/icons-react"; + +export const mediaMissingWidget: WidgetDefinition = { + icon: IconMovie, + name: "Media Missing & Queue", + description: "Track missing media and active downloads from Radarr and Sonarr", + path: "../../widgets/media-missing", + configuration: { + items: [ + { + name: "Show Missing", + description: "Display the missing media tab, listing movies or episodes not yet downloaded", + values: { type: "boolean" }, + defaultValue: "true", + }, + { + name: "Show Queued", + description: "Display the download queue tab with progress for each item", + values: { type: "boolean" }, + defaultValue: "true", + }, + { + name: "Page Size", + description: "Maximum number of items to fetch per integration", + values: { type: "string" }, + defaultValue: "10", + }, + ], + }, +}; diff --git a/apps/docs/docs/widgets/patchmon/index.mdx b/apps/docs/docs/widgets/patchmon/index.mdx new file mode 100644 index 000000000..d16ab8d68 --- /dev/null +++ b/apps/docs/docs/widgets/patchmon/index.mdx @@ -0,0 +1,89 @@ +--- +title: 'PatchMon' +description: 'Displays host patch statistics from PatchMon including update and security counts.' +hide_title: true +--- + +import { WidgetHeader } from '@site/src/components/widgets/header'; +import { AddingWidget } from '@site/src/components/widgets/adding'; +import { WidgetConfig } from '@site/src/components/widgets/configuration'; +import { WidgetIntegrations } from '@site/src/components/widgets/integrations'; +import { patchmonWidget } from '.'; +import { patchmonIntegration } from '@site/docs/integrations/patchmon'; + + + + +This widget displays patch statistics from your PatchMon instance, including total hosts, hosts needing updates, and security update counts. Each stat is shown with a dedicated icon and severity-tinted card background for quick visual scanning. + +PatchMon does not render a built-in widget title. To add a label on your board, open **Edit item → Advanced options → Title** — the same optional overlay badge used by other Homarr widgets. + +### Authentication + +:::note +This widget requires a **signed-in Homarr session**. Anonymous visitors cannot load PatchMon data, even when the board is [public](/docs/management/boards#access-control). Placing this widget on a public board does not expose patch or security statistics to guests. + +Stat visibility toggles in the widget settings control **display only**. Authenticated users with integration query access still receive the full API payload regardless of which stats are hidden in the UI. +::: + +### Compliance hero + +When **Show compliance hero** is enabled (default), the widget displays a ring showing the percentage of hosts that are fully up to date. The ring color follows the same threshold rules as the "Up-to-Date Hosts" stat. When the widget is **2 or more columns wide**, the hero also shows the "Patch Compliance" label and exact host count (e.g. "27 / 42") beside the ring. + +In **1-column-wide** layouts (such as 1×1), the compliance hero switches to a ring-only view so useful compliance data remains visible in tight spaces. If the compliance hero is disabled, compact widgets fall back to the next selected content: OS distribution when enabled, then the first enabled stat. + +### Threshold colors + +When **Enable threshold colors** is on (default), stat values and card backgrounds are colored for at-a-glance severity: + +| Stat | Green | Yellow | Red | +|------|-------|--------|-----| +| Needs Updates | 0 | 1–4 | 5+ | +| Security Updates | 0 | 1–9 | 10+ | +| Hosts with Security Updates | 0 | — | 1+ | +| Up-to-Date Hosts | 100% of total | 90–99% | below 90% | +| Outdated Packages | 0 | 1–49 | 50+ | + +Enable **Advanced thresholds** to override warning and critical values per stat, using either absolute counts or percentages of total hosts. + +### OS distribution + +When **Show OS distribution** is enabled, the widget shows a breakdown of monitored hosts by operating system. Two display modes are available: + +- **Horizontal bars** (default) — a scrollable list of colored progress bars showing each OS's share +- **Donut chart** — a compact donut chart visualization that scales down in narrow layouts + +The OS distribution limit defaults to **All**. A widget size of **2×3** or larger is recommended when using this section alongside the default stats, but compact 1×1 and 2×1 layouts can still show multiple OS rows when OS distribution is the primary selected content. Wider donut layouts can also show a compact legend beside the chart. + +### Responsive sizing + +The widget adapts its layout based on the available space: + +| Widget size | Layout | +|-------------|--------| +| **1×1** | Primary selected content: compliance ring (no label), compact OS distribution, or up to two enabled stats | +| **2×1** / **1×2** | Compact primary content; **2×1** shows compliance label and host ratio beside the ring; **1×2** uses ring-only hero | +| **2×2** (default) | Compliance hero, stat grid with icons, and tinted cards | +| **2×3** / **3×2** | Hero, stats, OS distribution, and "last updated" footer | +| **3×3** or larger | Full layout with up to 4-column stat grid, donut chart, and optional OS legend | + +### Last updated + +When the widget has enough space, a "last updated" timestamp appears at the bottom showing how recently PatchMon refreshed its data. Narrow layouts use a shortened timestamp such as `5m ago`; wider short layouts can show the full translated text. + +### Supported Integrations + + + +### Adding the widget + + + +### Configuration + diff --git a/apps/docs/docs/widgets/patchmon/index.ts b/apps/docs/docs/widgets/patchmon/index.ts new file mode 100644 index 000000000..0080dd8b4 --- /dev/null +++ b/apps/docs/docs/widgets/patchmon/index.ts @@ -0,0 +1,200 @@ +import { WidgetDefinition } from "@site/src/types"; +import { IconShieldCheck } from "@tabler/icons-react"; + +export const patchmonWidget: WidgetDefinition = { + icon: IconShieldCheck, + name: "PatchMon", + description: + "Displays host patch statistics including total hosts, hosts needing updates, and security update counts.", + path: "../../widgets/patchmon", + configuration: { + items: [ + { + name: "Show compliance hero", + description: "Display a compliance ring showing the percentage of up-to-date hosts", + values: { type: "boolean" }, + defaultValue: "yes", + }, + { + name: "Show total hosts", + description: "Displays the total number of monitored hosts", + values: { type: "boolean" }, + defaultValue: "yes", + }, + { + name: "Show hosts needing updates", + description: "Displays hosts with at least one outdated package", + values: { type: "boolean" }, + defaultValue: "yes", + }, + { + name: "Show security updates", + description: "Displays the total number of security updates available", + values: { type: "boolean" }, + defaultValue: "yes", + }, + { + name: "Show up-to-date hosts", + description: "Displays hosts with zero outdated packages", + values: { type: "boolean" }, + defaultValue: "no", + }, + { + name: "Show hosts with security updates", + description: "Displays hosts requiring security patches", + values: { type: "boolean" }, + defaultValue: "no", + }, + { + name: "Show recent updates (24h)", + description: "Displays the number of updates applied in the last 24 hours", + values: { type: "boolean" }, + defaultValue: "no", + }, + { + name: "Show outdated packages", + description: "Displays the total number of outdated packages across all hosts", + values: { type: "boolean" }, + defaultValue: "no", + }, + { + name: "Show repositories", + description: "Displays the number of active repositories being monitored", + values: { type: "boolean" }, + defaultValue: "no", + }, + { + name: "Enable threshold colors", + description: "Colors stat values green, yellow, or red based on built-in severity presets", + values: { type: "boolean" }, + defaultValue: "yes", + }, + { + name: "Advanced thresholds", + description: "Customize warning and critical thresholds per stat (absolute count or % of total hosts)", + values: { type: "boolean" }, + defaultValue: "no", + }, + { + name: "Needs updates threshold mode", + description: + "Compare the needs-updates count as an absolute value or percentage of total hosts. Shown when threshold colors and advanced thresholds are enabled.", + values: { type: "select", options: ["Absolute count", "Percent of total hosts"] }, + defaultValue: "Absolute count", + }, + { + name: "Needs updates warning threshold", + description: "Enter yellow when the value reaches this threshold", + values: "Integer ≥ 0", + defaultValue: "1", + }, + { + name: "Needs updates critical threshold", + description: "Enter red when the value reaches this threshold", + values: "Integer ≥ 0", + defaultValue: "5", + }, + { + name: "Security updates threshold mode", + description: + "Compare the security-updates count as an absolute value or percentage of total hosts. Shown when threshold colors and advanced thresholds are enabled.", + values: { type: "select", options: ["Absolute count", "Percent of total hosts"] }, + defaultValue: "Absolute count", + }, + { + name: "Security updates warning threshold", + description: "Enter yellow when the value reaches this threshold", + values: "Integer ≥ 0", + defaultValue: "1", + }, + { + name: "Security updates critical threshold", + description: "Enter red when the value reaches this threshold", + values: "Integer ≥ 0", + defaultValue: "10", + }, + { + name: "Hosts with security updates threshold mode", + description: + "Compare the host count as an absolute value or percentage of total hosts. Shown when threshold colors and advanced thresholds are enabled.", + values: { type: "select", options: ["Absolute count", "Percent of total hosts"] }, + defaultValue: "Absolute count", + }, + { + name: "Hosts with security updates warning threshold", + description: "Enter yellow when the value reaches this threshold", + values: "Integer ≥ 0", + defaultValue: "1", + }, + { + name: "Hosts with security updates critical threshold", + description: "Enter red when the value reaches this threshold", + values: "Integer ≥ 0", + defaultValue: "1", + }, + { + name: "Up-to-date hosts threshold mode", + description: + "Compare the up-to-date host count as an absolute value or percentage of total hosts. Shown when threshold colors and advanced thresholds are enabled.", + values: { type: "select", options: ["Absolute count", "Percent of total hosts"] }, + defaultValue: "Percent of total hosts", + }, + { + name: "Up-to-date hosts warning threshold", + description: "Enter yellow when the value is at or above this threshold but below full coverage", + values: "Integer ≥ 0", + defaultValue: "90", + }, + { + name: "Up-to-date hosts critical threshold", + description: "Enter red when the value falls below this threshold", + values: "Integer ≥ 0", + defaultValue: "90", + }, + { + name: "Outdated packages threshold mode", + description: + "Compare the outdated-packages count as an absolute value or percentage of total hosts. Shown when threshold colors, advanced thresholds, and Show outdated packages are enabled.", + values: { type: "select", options: ["Absolute count", "Percent of total hosts"] }, + defaultValue: "Absolute count", + }, + { + name: "Outdated packages warning threshold", + description: "Enter yellow when the value reaches this threshold", + values: "Integer ≥ 0", + defaultValue: "1", + }, + { + name: "Outdated packages critical threshold", + description: "Enter red when the value reaches this threshold", + values: "Integer ≥ 0", + defaultValue: "50", + }, + { + name: "Show OS distribution", + description: "Displays a horizontal bar breakdown of monitored hosts by operating system", + values: { type: "boolean" }, + defaultValue: "no", + }, + { + name: "OS display mode", + description: + "Choose how to display the operating system distribution. Shown when Show OS distribution is enabled.", + values: { type: "select", options: ["Horizontal bars", "Donut chart"] }, + defaultValue: "Horizontal bars", + }, + { + name: "OS distribution limit", + description: "Maximum number of operating systems to display in the distribution section", + values: { type: "select", options: ["Top 3", "Top 5", "Top 10", "All"] }, + defaultValue: "All", + }, + { + name: "Show OS version", + description: "Append the OS version to each distribution label when available", + values: { type: "boolean" }, + defaultValue: "yes", + }, + ], + }, +}; diff --git a/apps/docs/docs/widgets/releases/index.mdx b/apps/docs/docs/widgets/releases/index.mdx index 7808756f0..faf386a13 100644 --- a/apps/docs/docs/widgets/releases/index.mdx +++ b/apps/docs/docs/widgets/releases/index.mdx @@ -1,69 +1,44 @@ --- -title: 'Releases' -description: 'Displays a list of the current version of the given repositories with the given version regex.' +title: "Releases" +description: "Displays a list of the current version of the given repositories with the given version regex." hide_title: true --- -import { AddingWidget } from '@site/src/components/widgets/adding'; -import { WidgetConfig } from '@site/src/components/widgets/configuration'; -import { WidgetHeader } from '@site/src/components/widgets/header'; -import { WidgetIntegrations } from '@site/src/components/widgets/integrations'; -import { releasesWidget } from '.'; -import { githubIntegration } from '@site/docs/integrations/github'; -import { gitlabIntegration } from '@site/docs/integrations/gitlab'; -import { dockerHubIntegration } from '@site/docs/integrations/docker-hub'; -import { npmIntegration } from '@site/docs/integrations/npm'; -import { codebergIntegration } from '@site/docs/integrations/codeberg'; +import { AddingWidget } from "@site/src/components/widgets/adding"; +import { WidgetConfig } from "@site/src/components/widgets/configuration"; +import { WidgetHeader } from "@site/src/components/widgets/header"; +import { releasesWidget } from "."; - + -The releases widget enables you to track a list of repositories and see their respective latest release. -It also supports importing releases from your local docker containers. +The releases widget enables you to track a list of repositories and see their respective latest release. +It also supports importing releases from your local docker containers. ### Screenshots -import collapsibleCard from './img/collapsible.png'; -import fullCard from './img/full.png'; -import noDetailsCard from './img/no-details.png'; -import { quayIntegration } from '@site/docs/integrations/quay'; -import { gitHubContainerRegistryIntegration } from '@site/docs/integrations/github-containerregistry'; -import { linuxServerIoIntegration } from '@site/docs/integrations/linux-server-io'; +import collapsibleCard from "./img/collapsible.png"; +import fullCard from "./img/full.png"; +import noDetailsCard from "./img/no-details.png"; -
+
releases full releases collapsible releases no details
-### Supported Integrations +### Supported Providers - +GitHub, Docker Hub, GitLab, NPM, Codeberg, GitHub Container Registry, Quay, and LinuxServer.io are configured per repository in the widget. + +For GitHub Container Registry, use `owner/package` identifiers such as `homarr-labs/homarr`. +Full image references such as `ghcr.io/homarr-labs/homarr:latest` are also accepted. ### Adding the widget ### Configuration + :::note @@ -74,4 +49,3 @@ Setting the Prefix to `v`, precision to `2` and the Suffix to `-alpha`, will mat Setting the Prefix to `v`, precision to `3`, will match `v5.2.0` but not `v1.0.2-alpha` or `Beta v5.2.1`. ::: - diff --git a/apps/docs/docs/widgets/releases/index.ts b/apps/docs/docs/widgets/releases/index.ts index dbe3c1271..1f2270e82 100644 --- a/apps/docs/docs/widgets/releases/index.ts +++ b/apps/docs/docs/widgets/releases/index.ts @@ -54,16 +54,23 @@ export const releasesWidget: WidgetDefinition = { items: [ { name: "Provider", - description: "Integration to fetch the repository from.", - values: "Select from the available integrations.", + description: "Provider to fetch the repository or package releases from.", + values: "Select from the available providers.", defaultValue: "-", }, { name: "Identifier", - description: 'The identifier of the repository, e.g. "homarr-labs/homarr".', - values: "Any valid repository identifier.", + description: 'The identifier of the repository or package, e.g. "homarr-labs/homarr".', + values: + 'Any valid provider identifier. GHCR also accepts full image refs like "ghcr.io/owner/package:tag".', defaultValue: "-", }, + { + name: "Provider URL", + description: "Base URL for the provider.", + values: "Any valid URL.", + defaultValue: "Provider default", + }, { name: "Name", description: "Display name of the repository.", diff --git a/apps/docs/docs/widgets/system-disks/index.mdx b/apps/docs/docs/widgets/system-disks/index.mdx new file mode 100644 index 000000000..b3be87e1d --- /dev/null +++ b/apps/docs/docs/widgets/system-disks/index.mdx @@ -0,0 +1,51 @@ +--- +title: "System disks" +description: "Disk usage of your system" +hide_title: true +--- + +import { WidgetHeader } from "@site/src/components/widgets/header"; +import { AddingWidget } from "@site/src/components/widgets/adding"; +import { WidgetConfig } from "@site/src/components/widgets/configuration"; +import { WidgetIntegrations } from "@site/src/components/widgets/integrations"; +import { systemDisksWidget } from "."; +import { dashDotIntegration } from "@site/docs/integrations/dash-dot"; +import { openMediaVaultIntegration } from "@site/docs/integrations/open-media-vault"; +import { truenasIntegration } from "@site/docs/integrations/truenas"; +import { unraidIntegration } from "@site/docs/integrations/unraid"; +import { synologyIntegration } from "@site/docs/integrations/synology"; + + + +This widget displays filesystem and SMART disk usage for supported server and NAS integrations. + +### Supported Integrations + + + +### Adding the widget + + + +### Configuration + + diff --git a/apps/docs/docs/widgets/system-disks/index.ts b/apps/docs/docs/widgets/system-disks/index.ts new file mode 100644 index 000000000..8dd68f057 --- /dev/null +++ b/apps/docs/docs/widgets/system-disks/index.ts @@ -0,0 +1,41 @@ +import { WidgetDefinition } from "@site/src/types"; +import { IconServer2 } from "@tabler/icons-react"; + +export const systemDisksWidget: WidgetDefinition = { + icon: IconServer2, + name: "System disks", + description: "Disk usage of your system", + path: "../../widgets/system-disks", + configuration: { + items: [ + { + name: "Show temperature if available", + description: "Display disk temperature when the integration provides it", + values: { type: "boolean" }, + defaultValue: "Yes", + }, + { + name: "Display mode", + description: "How disk usage values are shown", + values: { + type: "select", + options: ["Percentage (e.g., 76%)", "Absolute values (e.g., 800GB / 1TB)", "Free space percentage (e.g., 24% free)"], + }, + defaultValue: "Percentage (e.g., 76%)", + }, + { + name: "Show background usage bar", + description: "Fill the area below the usage value with a background bar", + values: { type: "boolean" }, + defaultValue: "Yes", + }, + { + name: "Visible storage volumes", + description: + "Only available when all selected integrations are Synology. Leave empty to show all detected storage volumes.", + values: "List of storage volumes", + defaultValue: "All volumes", + }, + ], + }, +}; diff --git a/apps/docs/docs/widgets/system-resources/index.mdx b/apps/docs/docs/widgets/system-resources/index.mdx index 143e2a9cf..16ad19750 100644 --- a/apps/docs/docs/widgets/system-resources/index.mdx +++ b/apps/docs/docs/widgets/system-resources/index.mdx @@ -1,57 +1,69 @@ --- -title: 'System Resources' -description: 'Displays CPU, RAM and network of your host in real time' +title: "System Resources" +description: "Displays CPU, RAM and network of your host in real time" hide_title: true --- -import { WidgetHeader } from '@site/src/components/widgets/header'; -import { AddingWidget } from '@site/src/components/widgets/adding'; -import { WidgetConfig } from '@site/src/components/widgets/configuration'; -import { WidgetIntegrations } from '@site/src/components/widgets/integrations'; -import { systemResourcesWidget } from '.'; -import { dashDotIntegration } from '@site/docs/integrations/dash-dot'; -import { glancesIntegration } from '@site/docs/integrations/glances'; -import { openMediaVaultIntegration } from '@site/docs/integrations/open-media-vault'; -import { proxmoxIntegration } from '@site/docs/integrations/proxmox'; +import { WidgetHeader } from "@site/src/components/widgets/header"; +import { AddingWidget } from "@site/src/components/widgets/adding"; +import { WidgetConfig } from "@site/src/components/widgets/configuration"; +import { WidgetIntegrations } from "@site/src/components/widgets/integrations"; +import { systemResourcesWidget } from "."; +import { dashDotIntegration } from "@site/docs/integrations/dash-dot"; +import { glancesIntegration } from "@site/docs/integrations/glances"; +import { openMediaVaultIntegration } from "@site/docs/integrations/open-media-vault"; +import { proxmoxIntegration } from "@site/docs/integrations/proxmox"; - - + Displays CPU, RAM and network of your host in real time ### Screenshots -import network from './img/network.png'; -import cpu from './img/cpu.png'; -import memory from './img/memory.png'; -import cpuAndMemory from './img/cpu-and-memory.png'; -import { truenasIntegration } from '@site/docs/integrations/truenas'; +import network from "./img/network.png"; +import cpu from "./img/cpu.png"; +import memory from "./img/memory.png"; +import cpuAndMemory from "./img/cpu-and-memory.png"; +import { truenasIntegration } from "@site/docs/integrations/truenas"; +import { synologyIntegration } from "@site/docs/integrations/synology"; -
-
+
+
system-resources-widget network-only icon-label -
+
system-resources-widget cpu-only no-label item-title system-resources-widget memory-only text-label
- system-resources-widget cpu-and-memory icon-text-label + system-resources-widget cpu-and-memory icon-text-label
### Supported Integrations - + ### Adding the widget @@ -59,4 +71,4 @@ import { truenasIntegration } from '@site/docs/integrations/truenas'; ### Configuration - \ No newline at end of file + diff --git a/apps/docs/docs/widgets/timetable/index.mdx b/apps/docs/docs/widgets/timetable/index.mdx index fe9a1b11c..e9bdfd326 100644 --- a/apps/docs/docs/widgets/timetable/index.mdx +++ b/apps/docs/docs/widgets/timetable/index.mdx @@ -1,44 +1,40 @@ --- -title: 'Timetable' -description: 'Displays departure times for a station.' +title: "Timetable" +description: "Displays departure times for a station." hide_title: true --- -import { AddingWidget } from '@site/src/components/widgets/adding'; -import { WidgetConfig } from '@site/src/components/widgets/configuration'; -import { WidgetHeader } from '@site/src/components/widgets/header'; -import { WidgetIntegrations } from '@site/src/components/widgets/integrations'; -import { timetableWidget } from '.'; -import { searchChIntegration } from '@site/docs/integrations/search-ch'; +import { AddingWidget } from "@site/src/components/widgets/adding"; +import { WidgetConfig } from "@site/src/components/widgets/configuration"; +import { WidgetHeader } from "@site/src/components/widgets/header"; +import { timetableWidget } from "."; - - + The Timetable widget provides real-time departure times for a selected station. ### Screenshots -import trainDepartures from './img/train-departures.png'; -import streetDepartures from './img/street-departures.png'; +import trainDepartures from "./img/train-departures.png"; +import streetDepartures from "./img/street-departures.png";
- timetable-widget-train-departures - timetable-widget-bus-and-tram-departures + timetable-widget-train-departures + timetable-widget-bus-and-tram-departures
### Adding the widget -### Supported Integrations - - - ### Configuration + diff --git a/apps/docs/docs/widgets/timetable/index.ts b/apps/docs/docs/widgets/timetable/index.ts index 6f36349ee..1cefb6bcd 100644 --- a/apps/docs/docs/widgets/timetable/index.ts +++ b/apps/docs/docs/widgets/timetable/index.ts @@ -8,6 +8,12 @@ export const timetableWidget: WidgetDefinition = { path: "../../widgets/timetable", configuration: { items: [ + { + name: "Search.ch URL", + description: "Base URL for the Search.ch timetable API.", + values: "Any valid URL.", + defaultValue: "https://search.ch", + }, { name: "Station", description: "Select the station", diff --git a/apps/docs/docs/widgets/traefik/index.mdx b/apps/docs/docs/widgets/traefik/index.mdx new file mode 100644 index 000000000..dc9f6a6c4 --- /dev/null +++ b/apps/docs/docs/widgets/traefik/index.mdx @@ -0,0 +1,35 @@ +--- +title: "Traefik" +description: "Displays Traefik router, service, middleware, and entry point counts." +hide_title: true +--- + +import { WidgetHeader } from "@site/src/components/widgets/header"; +import { AddingWidget } from "@site/src/components/widgets/adding"; +import { WidgetConfig } from "@site/src/components/widgets/configuration"; +import { WidgetIntegrations } from "@site/src/components/widgets/integrations"; +import { traefikWidget } from "."; +import { traefikIntegration } from "@site/docs/integrations/traefik"; + + + +This widget displays a compact overview of your Traefik instance, including total routers, entry points, protocol-specific router/service/middleware counts, and warning or error totals when Traefik reports them. + +### Supported Integrations + + + +### Adding the widget + + + +### Configuration + + diff --git a/apps/docs/docs/widgets/traefik/index.ts b/apps/docs/docs/widgets/traefik/index.ts new file mode 100644 index 000000000..d55216eef --- /dev/null +++ b/apps/docs/docs/widgets/traefik/index.ts @@ -0,0 +1,31 @@ +import { WidgetDefinition } from "@site/src/types"; +import { IconRoute } from "@tabler/icons-react"; + +export const traefikWidget: WidgetDefinition = { + icon: IconRoute, + name: "Traefik", + description: "Overview of Traefik routers, services, middlewares, and entry points.", + path: "../../widgets/traefik", + configuration: { + items: [ + { + name: "Show TCP", + description: "Display TCP routers, services, and middlewares.", + values: { type: "boolean" }, + defaultValue: "yes", + }, + { + name: "Show UDP", + description: "Display UDP routers and services.", + values: { type: "boolean" }, + defaultValue: "yes", + }, + { + name: "Show entry points", + description: "Display the configured Traefik entry point names.", + values: { type: "boolean" }, + defaultValue: "yes", + }, + ], + }, +}; diff --git a/apps/docs/docsearch.config.js b/apps/docs/docsearch.config.js index 0af932a97..2cf493485 100644 --- a/apps/docs/docsearch.config.js +++ b/apps/docs/docsearch.config.js @@ -1,18 +1,25 @@ // Algolia DocSearch crawler for homarr.dev // Crawler ID: cd77a285-2756-4557-bf21-ee703748df15 -// DocSearch-managed crawlers: paste this in the Algolia crawler editor or send to DocSearch support. -// Template: https://docsearch.algolia.com/docs/templates/#docusaurus-v3-template -// Markdown indexing: https://www.algolia.com/doc/guides/algolia-ai/askai/guides/markdown-indexing +// Paste this in the Algolia crawler editor. new Crawler({ appId: "N69WSPZTID", apiKey: "YOUR_CRAWLER_ADMIN_API_KEY", - rateLimit: 8, - maxDepth: 10, + indexPrefix: "", + rateLimit: 32, + maxDepth: 4, startUrls: ["https://homarr.dev/"], sitemaps: ["https://homarr.dev/sitemap.xml"], ignoreCanonicalTo: true, discoveryPatterns: ["https://homarr.dev/**"], + exclusionPatterns: [ + "https://homarr.dev/docs/tags/**", + "https://homarr.dev/blog/tags/**", + "https://homarr.dev/blog/authors/**", + "https://homarr.dev/blog/archive/**", + "https://homarr.dev/search/**", + "https://homarr.dev/docs/category/**", + ], actions: [ { indexName: "Docusaurus", @@ -105,7 +112,6 @@ new Crawler({ advancedSyntax: true, attributeCriteriaComputedByMinProximity: true, removeWordsIfNoResults: "allOptional", - separatorsToIndex: "_", }, markdown: { attributesForFaceting: ["lang", "language", "version", "docusaurus_tag"], @@ -119,4 +125,10 @@ new Crawler({ advancedSyntax: false, }, }, + apiKey: "YOUR_CRAWLER_ADMIN_API_KEY", + schedule: "on saturday", + renderJavaScript: false, + maxUrls: null, + cache: { enabled: false }, + safetyChecks: { beforeIndexPublishing: { maxLostRecordsPercentage: 99 } }, }); diff --git a/apps/docs/docusaurus.config.ts b/apps/docs/docusaurus.config.ts index 309b119f0..0edf8a8f3 100644 --- a/apps/docs/docusaurus.config.ts +++ b/apps/docs/docusaurus.config.ts @@ -8,7 +8,7 @@ const config: Config = { tagline: "A simple yet powerful dashboard for your server.", url: "https://homarr.dev", baseUrl: "/", - trailingSlash: true, + trailingSlash: undefined, favicon: "img/logo.png", organizationName: "homarr-labs", projectName: "homarr", @@ -79,8 +79,27 @@ const config: Config = { sitemap: { changefreq: "weekly", priority: 0.5, - ignorePatterns: ["/tags/**"], + ignorePatterns: ["/tags/**", "/docs/category/**"], filename: "sitemap.xml", + createSitemapItems: async ({ routes, siteConfig, defaultCreateSitemapItems }) => { + const items = await defaultCreateSitemapItems({ routes, siteConfig }); + return items.map((item) => { + const path = new URL(item.url).pathname; + if (path === "/" || path === "") { + return { ...item, priority: 1.0, changefreq: "weekly" }; + } + if (path.startsWith("/docs/")) { + return { ...item, priority: 0.7, changefreq: "monthly" }; + } + if (path.startsWith("/blog/")) { + return { ...item, priority: 0.3, changefreq: "never" }; + } + if (path.startsWith("/about-us")) { + return { ...item, priority: 0.4, changefreq: "yearly" }; + } + return item; + }); + }, }, } satisfies Preset.Options, ], diff --git a/apps/docs/package.json b/apps/docs/package.json index f4c7655b9..586fa53b6 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -32,19 +32,19 @@ "@scalar/api-reference-react": "catalog:", "@signalwire/docusaurus-plugin-llms-txt": "^1.2.2", "@tabler/icons-react": "catalog:", - "@tailwindcss/postcss": "^4.3.1", + "@tailwindcss/postcss": "^4.3.2", "clsx": "catalog:", "dayjs": "catalog:", "docusaurus-plugin-image-zoom": "^3.0.1", "leader-line-new": "^1.1.9", - "postcss": "^8.5.15", + "postcss": "^8.5.16", "posthog-docusaurus": "^2.0.5", "prism-react-renderer": "^2.4.1", "random-word-slugs": "^0.1.7", "react": "catalog:", "react-dom": "catalog:", - "swiper": "^12.2.0", - "tailwindcss": "^4.3.1" + "swiper": "^14.0.1", + "tailwindcss": "^4.3.2" }, "devDependencies": { "@docusaurus/module-type-aliases": "^3.10.1", diff --git a/apps/docs/src/components/integrations/secrets.tsx b/apps/docs/src/components/integrations/secrets.tsx index 2fc09fc6d..e03e59430 100644 --- a/apps/docs/src/components/integrations/secrets.tsx +++ b/apps/docs/src/components/integrations/secrets.tsx @@ -47,6 +47,16 @@ const secretKinds = { description: "The Secret part of the API Key for authentication.", icon: IconPassword, }, + patchmonApiKey: { + name: "API Key", + description: "The API key from PatchMon's GetHomepage integration.", + icon: IconKey, + }, + patchmonApiSecret: { + name: "API Secret", + description: "The API secret from PatchMon's GetHomepage integration.", + icon: IconPassword, + }, githubAppId: { name: "App ID", description: "The ID of the GitHub App", icon: IconCode }, githubInstallationId: { name: "Installation ID", diff --git a/apps/docs/src/css/custom.css b/apps/docs/src/css/custom.css index a27d0e558..93d4ab319 100644 --- a/apps/docs/src/css/custom.css +++ b/apps/docs/src/css/custom.css @@ -1,8 +1,7 @@ +@import url("https://api.fontshare.com/v2/css?f[]=work-sans@2,1&display=swap"); @import "tailwindcss/theme" layer(theme); @import "tailwindcss/utilities" layer(utilities); -@import url(https://api.fontshare.com/v2/css?f[]=work-sans@2,1&display=swap); - @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *)); @theme { diff --git a/apps/docs/static/api/open-api-schema.json b/apps/docs/static/api/open-api-schema.json index 50384fba1..ca2f93d64 100644 --- a/apps/docs/static/api/open-api-schema.json +++ b/apps/docs/static/api/open-api-schema.json @@ -1 +1 @@ -{"openapi":"3.1.0","info":{"title":"Homarr API documentation","version":"1.0.0"},"servers":[{"url":"http://localhost:3000"}],"externalDocs":{"url":"https://homarr.dev"},"paths":{"/api/apps/paginated":{"get":{"operationId":"appRouter-getPaginated","tags":["apps"],"security":[{"apikey":[]}],"parameters":[{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"pageSize","schema":{"default":10,"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},{"in":"query","name":"page","schema":{"default":1,"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"iconUrl":{"type":"string"},"href":{"anyOf":[{"type":"string"},{"type":"null"}]},"pingUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","description","iconUrl","href","pingUrl"],"additionalProperties":false}},"totalCount":{"type":"number"}},"required":["items","totalCount"],"additionalProperties":false}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/apps":{"get":{"operationId":"appRouter-all","tags":["apps"],"security":[{"apikey":[]}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"iconUrl":{"type":"string"},"href":{"anyOf":[{"type":"string"},{"type":"null"}]},"pingUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","description","iconUrl","href","pingUrl"],"additionalProperties":false}}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}},"post":{"operationId":"appRouter-create","tags":["apps"],"security":[{"apikey":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":64},"description":{"anyOf":[{"type":"string","maxLength":512},{"type":"null"}]},"iconUrl":{"type":"string","minLength":1},"href":{"anyOf":[{"anyOf":[{"type":"string","pattern":"^(?!javascript)[a-zA-Z]*:\\/\\/"},{"type":"string","const":""}]},{"type":"null"}]},"pingUrl":{"anyOf":[{"anyOf":[{"type":"string","pattern":"^https?:\\/\\/"},{"type":"string","const":""}]},{"type":"null"}]}},"required":["name","description","iconUrl","href","pingUrl"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"allOf":[{"type":"object","properties":{"appId":{"type":"string"}},"required":["appId"],"additionalProperties":false},{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"iconUrl":{"type":"string"},"href":{"anyOf":[{"type":"string"},{"type":"null"}]},"pingUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","description","iconUrl","href","pingUrl"],"additionalProperties":false}]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/apps/search":{"get":{"operationId":"appRouter-search","tags":["apps"],"security":[{"apikey":[]}],"parameters":[{"in":"query","name":"query","schema":{"type":"string"},"required":true},{"in":"query","name":"limit","schema":{"default":10,"type":"number","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"iconUrl":{"type":"string"},"href":{"anyOf":[{"type":"string"},{"type":"null"}]},"pingUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","description","iconUrl","href","pingUrl"],"additionalProperties":false}}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/apps/selectable":{"get":{"operationId":"appRouter-selectable","tags":["apps"],"security":[{"apikey":[]}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"iconUrl":{"type":"string"},"href":{"anyOf":[{"type":"string"},{"type":"null"}]},"pingUrl":{"anyOf":[{"type":"string"},{"type":"null"}]},"description":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","iconUrl","href","pingUrl","description"],"additionalProperties":false}}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/apps/{id}":{"get":{"operationId":"appRouter-byId","tags":["apps"],"security":[{"apikey":[]}],"parameters":[{"in":"path","name":"id","schema":{"type":"string"},"required":true}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"iconUrl":{"type":"string"},"href":{"anyOf":[{"type":"string"},{"type":"null"}]},"pingUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","description","iconUrl","href","pingUrl"],"additionalProperties":false}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}},"patch":{"operationId":"appRouter-update","tags":["apps"],"security":[{"apikey":[]}],"parameters":[{"in":"path","name":"id","schema":{"type":"string"},"required":true}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":64},"description":{"anyOf":[{"type":"string","maxLength":512},{"type":"null"}]},"iconUrl":{"type":"string","minLength":1},"href":{"anyOf":[{"anyOf":[{"type":"string","pattern":"^(?!javascript)[a-zA-Z]*:\\/\\/"},{"type":"string","const":""}]},{"type":"null"}]},"pingUrl":{"anyOf":[{"anyOf":[{"type":"string","pattern":"^https?:\\/\\/"},{"type":"string","const":""}]},{"type":"null"}]}},"required":["name","description","iconUrl","href","pingUrl"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}},"delete":{"operationId":"appRouter-delete","tags":["apps"],"security":[{"apikey":[]}],"parameters":[{"in":"path","name":"id","schema":{"type":"string"},"required":true}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/info":{"get":{"operationId":"infoRouter-getInfo","tags":["info"],"security":[{"apikey":[]}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"version":{"type":"string"}},"required":["version"],"additionalProperties":false}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/invites":{"get":{"operationId":"inviteRouter-getAll","tags":["invites"],"security":[{"apikey":[]}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"expirationDate":{"type":"string"},"creator":{"type":"object","properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}]},"id":{"type":"string"}},"required":["name","id"],"additionalProperties":false}},"required":["id","expirationDate","creator"],"additionalProperties":false}}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}},"post":{"operationId":"inviteRouter-createInvite","tags":["invites"],"security":[{"apikey":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"expirationDate":{"type":"string"}},"required":["expirationDate"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"token":{"type":"string"}},"required":["id","token"],"additionalProperties":false}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/invites/{id}":{"delete":{"operationId":"inviteRouter-deleteInvite","tags":["invites"],"security":[{"apikey":[]}],"parameters":[{"in":"path","name":"id","schema":{"type":"string"},"required":true}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"not":{}}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/users":{"post":{"operationId":"userRouter-create","tags":["users"],"security":[{"apikey":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"username":{"type":"string","minLength":3,"maxLength":255},"password":{"type":"string","minLength":8,"maxLength":255},"confirmPassword":{"type":"string"},"email":{"anyOf":[{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"string","minLength":0,"maxLength":0}]},"groupIds":{"type":"array","items":{"type":"string"}}},"required":["username","password","confirmPassword","groupIds"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}},"get":{"operationId":"userRouter-getAll","tags":["users"],"security":[{"apikey":[]}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"anyOf":[{"type":"string"},{"type":"null"}]},"email":{"anyOf":[{"type":"string"},{"type":"null"}]},"emailVerified":{"anyOf":[{"type":"string"},{"type":"null"}]},"image":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","email","emailVerified","image"],"additionalProperties":false}}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/users/profileImage":{"put":{"operationId":"userRouter-setProfileImage","tags":["users"],"security":[{"apikey":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string"},"image":{"anyOf":[{"type":"string","maxLength":350000,"pattern":"^data:image\\/(png|jpeg|gif|webp);base64,[A-Za-z0-9/+]+=*$"},{"type":"null"}]}},"required":["userId","image"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/users/selectable":{"get":{"operationId":"userRouter-selectable","tags":["users"],"security":[{"apikey":[]}],"parameters":[{"in":"query","name":"excludeExternalProviders","schema":{"default":false,"type":"boolean"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"anyOf":[{"type":"string"},{"type":"null"}]},"image":{"anyOf":[{"type":"string"},{"type":"null"}]},"email":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","image","email"],"additionalProperties":false}}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/users/search":{"post":{"operationId":"userRouter-search","tags":["users"],"security":[{"apikey":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string"},"limit":{"default":10,"type":"number","minimum":1,"maximum":100}},"required":["query"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"anyOf":[{"type":"string"},{"type":"null"}]},"image":{"anyOf":[{"type":"string"},{"type":"null"}]},"email":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","image","email"],"additionalProperties":false}}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/users/{userId}":{"get":{"operationId":"userRouter-getById","tags":["users"],"security":[{"apikey":[]}],"parameters":[{"in":"path","name":"userId","schema":{"type":"string"},"required":true}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"anyOf":[{"type":"string"},{"type":"null"}]},"email":{"anyOf":[{"type":"string"},{"type":"null"}]},"emailVerified":{"anyOf":[{"type":"string"},{"type":"null"}]},"image":{"anyOf":[{"type":"string"},{"type":"null"}]},"provider":{"type":"string"},"homeBoardId":{"anyOf":[{"type":"string"},{"type":"null"}]},"mobileHomeBoardId":{"anyOf":[{"type":"string"},{"type":"null"}]},"firstDayOfWeek":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"pingIconsEnabled":{"type":"boolean"},"defaultSearchEngineId":{"anyOf":[{"type":"string"},{"type":"null"}]},"openSearchInNewTab":{"type":"boolean"},"ddgBangs":{"type":"boolean"},"completedManageTour":{"type":"boolean"},"completedBoardTour":{"type":"boolean"}},"required":["id","name","email","emailVerified","image","provider","homeBoardId","mobileHomeBoardId","firstDayOfWeek","pingIconsEnabled","defaultSearchEngineId","openSearchInNewTab","ddgBangs","completedManageTour","completedBoardTour"],"additionalProperties":false}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}},"delete":{"operationId":"userRouter-delete","tags":["users"],"security":[{"apikey":[]}],"parameters":[{"in":"path","name":"userId","schema":{"type":"string"},"required":true}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/users/profile":{"put":{"operationId":"userRouter-editProfile","tags":["users"],"security":[{"apikey":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","minLength":3,"maxLength":255},"email":{"anyOf":[{"anyOf":[{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"string","const":""}]},{"type":"null"}]}},"required":["id","name"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/users/{userId}/changePassword":{"patch":{"operationId":"userRouter-changePassword","tags":["users"],"security":[{"apikey":[]}],"parameters":[{"in":"path","name":"userId","schema":{"type":"string"},"required":true}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"previousPassword":{"type":"string","minLength":1},"password":{"type":"string","minLength":8,"maxLength":255},"confirmPassword":{"type":"string"}},"required":["previousPassword","password","confirmPassword"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/users/changeHome":{"patch":{"operationId":"userRouter-changeHomeBoards","tags":["users"],"security":[{"apikey":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"homeBoardId":{"anyOf":[{"type":"string"},{"type":"null"}]},"mobileHomeBoardId":{"anyOf":[{"type":"string"},{"type":"null"}]},"userId":{"type":"string"}},"required":["homeBoardId","mobileHomeBoardId","userId"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/users/changeSearchEngine":{"patch":{"operationId":"userRouter-changeDefaultSearchEngine","tags":["users"],"security":[{"apikey":[]}],"deprecated":true,"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"defaultSearchEngineId":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}]},"ddgBangsEnabled":{"type":"boolean"},"userId":{"type":"string"}},"required":["defaultSearchEngineId","ddgBangsEnabled","userId"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/users/search-preferences":{"patch":{"operationId":"userRouter-changeSearchPreferences","tags":["users"],"security":[{"apikey":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"defaultSearchEngineId":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}]},"openInNewTab":{"type":"boolean"},"ddgBangsEnabled":{"type":"boolean"},"userId":{"type":"string"}},"required":["defaultSearchEngineId","openInNewTab","ddgBangsEnabled","userId"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/users/changeScheme":{"patch":{"operationId":"userRouter-changeColorScheme","tags":["users"],"security":[{"apikey":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"colorScheme":{"type":"string","enum":["auto","light","dark"]}},"required":["colorScheme"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/users/ddg-bangs":{"patch":{"operationId":"userRouter-changeDdgBangs","tags":["users"],"security":[{"apikey":[]}],"deprecated":true,"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ddgBangs":{"type":"boolean"},"id":{"type":"string"}},"required":["ddgBangs","id"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/users/firstDayOfWeek":{"patch":{"operationId":"userRouter-changeFirstDayOfWeek","tags":["users"],"security":[{"apikey":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"firstDayOfWeek":{"type":"number","minimum":0,"maximum":6},"id":{"type":"string"}},"required":["firstDayOfWeek","id"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}}},"components":{"schemas":{"error.BAD_REQUEST":{"type":"object","properties":{"message":{"type":"string","description":"The error message","example":"Invalid input data"},"code":{"type":"string","description":"The error code","example":"BAD_REQUEST"},"issues":{"description":"An array of issues that were responsible for the error","example":[],"type":"array","items":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false}}},"required":["message","code"],"additionalProperties":false,"title":"Invalid input data error (400)","description":"The error information","example":{"code":"BAD_REQUEST","message":"Invalid input data","issues":[]}},"error.UNAUTHORIZED":{"type":"object","properties":{"message":{"type":"string","description":"The error message","example":"Authorization not provided"},"code":{"type":"string","description":"The error code","example":"UNAUTHORIZED"},"issues":{"description":"An array of issues that were responsible for the error","example":[],"type":"array","items":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false}}},"required":["message","code"],"additionalProperties":false,"title":"Authorization not provided error (401)","description":"The error information","example":{"code":"UNAUTHORIZED","message":"Authorization not provided","issues":[]}},"error.FORBIDDEN":{"type":"object","properties":{"message":{"type":"string","description":"The error message","example":"Insufficient access"},"code":{"type":"string","description":"The error code","example":"FORBIDDEN"},"issues":{"description":"An array of issues that were responsible for the error","example":[],"type":"array","items":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false}}},"required":["message","code"],"additionalProperties":false,"title":"Insufficient access error (403)","description":"The error information","example":{"code":"FORBIDDEN","message":"Insufficient access","issues":[]}},"error.NOT_FOUND":{"type":"object","properties":{"message":{"type":"string","description":"The error message","example":"Not found"},"code":{"type":"string","description":"The error code","example":"NOT_FOUND"},"issues":{"description":"An array of issues that were responsible for the error","example":[],"type":"array","items":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false}}},"required":["message","code"],"additionalProperties":false,"title":"Not found error (404)","description":"The error information","example":{"code":"NOT_FOUND","message":"Not found","issues":[]}},"error.INTERNAL_SERVER_ERROR":{"type":"object","properties":{"message":{"type":"string","description":"The error message","example":"Internal server error"},"code":{"type":"string","description":"The error code","example":"INTERNAL_SERVER_ERROR"},"issues":{"description":"An array of issues that were responsible for the error","example":[],"type":"array","items":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false}}},"required":["message","code"],"additionalProperties":false,"title":"Internal server error error (500)","description":"The error information","example":{"code":"INTERNAL_SERVER_ERROR","message":"Internal server error","issues":[]}}},"securitySchemes":{"apikey":{"type":"apiKey","name":"ApiKey","description":"API key which can be obtained in the Homarr administration dashboard","in":"header"}}}} \ No newline at end of file +{"openapi":"3.1.0","info":{"title":"Homarr API documentation","version":"1.1.0"},"servers":[{"url":"http://localhost:3000"}],"externalDocs":{"url":"https://homarr.dev"},"paths":{"/api/apps/paginated":{"get":{"operationId":"appRouter-getPaginated","tags":["apps"],"security":[{"apikey":[]}],"parameters":[{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"pageSize","schema":{"default":10,"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},{"in":"query","name":"page","schema":{"default":1,"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"iconUrl":{"type":"string"},"href":{"anyOf":[{"type":"string"},{"type":"null"}]},"pingUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","description","iconUrl","href","pingUrl"],"additionalProperties":false}},"totalCount":{"type":"number"}},"required":["items","totalCount"],"additionalProperties":false}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/apps":{"get":{"operationId":"appRouter-all","tags":["apps"],"security":[{"apikey":[]}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"iconUrl":{"type":"string"},"href":{"anyOf":[{"type":"string"},{"type":"null"}]},"pingUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","description","iconUrl","href","pingUrl"],"additionalProperties":false}}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}},"post":{"operationId":"appRouter-create","tags":["apps"],"security":[{"apikey":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":64},"description":{"anyOf":[{"type":"string","maxLength":512},{"type":"null"}]},"iconUrl":{"type":"string","minLength":1},"href":{"anyOf":[{"anyOf":[{"type":"string","pattern":"^(?!javascript)[a-zA-Z]*:\\/\\/"},{"type":"string","const":""}]},{"type":"null"}]},"pingUrl":{"anyOf":[{"anyOf":[{"type":"string","pattern":"^https?:\\/\\/"},{"type":"string","const":""}]},{"type":"null"}]}},"required":["name","description","iconUrl","href","pingUrl"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"allOf":[{"type":"object","properties":{"appId":{"type":"string"}},"required":["appId"],"additionalProperties":false},{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"iconUrl":{"type":"string"},"href":{"anyOf":[{"type":"string"},{"type":"null"}]},"pingUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","description","iconUrl","href","pingUrl"],"additionalProperties":false}]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/apps/search":{"get":{"operationId":"appRouter-search","tags":["apps"],"security":[{"apikey":[]}],"parameters":[{"in":"query","name":"query","schema":{"type":"string"},"required":true},{"in":"query","name":"limit","schema":{"default":10,"type":"number","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"iconUrl":{"type":"string"},"href":{"anyOf":[{"type":"string"},{"type":"null"}]},"pingUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","description","iconUrl","href","pingUrl"],"additionalProperties":false}}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/apps/selectable":{"get":{"operationId":"appRouter-selectable","tags":["apps"],"security":[{"apikey":[]}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"iconUrl":{"type":"string"},"href":{"anyOf":[{"type":"string"},{"type":"null"}]},"pingUrl":{"anyOf":[{"type":"string"},{"type":"null"}]},"description":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","iconUrl","href","pingUrl","description"],"additionalProperties":false}}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/apps/{id}":{"get":{"operationId":"appRouter-byId","tags":["apps"],"security":[{"apikey":[]}],"parameters":[{"in":"path","name":"id","schema":{"type":"string"},"required":true}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"iconUrl":{"type":"string"},"href":{"anyOf":[{"type":"string"},{"type":"null"}]},"pingUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","description","iconUrl","href","pingUrl"],"additionalProperties":false}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}},"patch":{"operationId":"appRouter-update","tags":["apps"],"security":[{"apikey":[]}],"parameters":[{"in":"path","name":"id","schema":{"type":"string"},"required":true}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":64},"description":{"anyOf":[{"type":"string","maxLength":512},{"type":"null"}]},"iconUrl":{"type":"string","minLength":1},"href":{"anyOf":[{"anyOf":[{"type":"string","pattern":"^(?!javascript)[a-zA-Z]*:\\/\\/"},{"type":"string","const":""}]},{"type":"null"}]},"pingUrl":{"anyOf":[{"anyOf":[{"type":"string","pattern":"^https?:\\/\\/"},{"type":"string","const":""}]},{"type":"null"}]}},"required":["name","description","iconUrl","href","pingUrl"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}},"delete":{"operationId":"appRouter-delete","tags":["apps"],"security":[{"apikey":[]}],"parameters":[{"in":"path","name":"id","schema":{"type":"string"},"required":true}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/boards":{"get":{"operationId":"boardRouter-getAllBoards","tags":["boards"],"security":[{"apikey":[]}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"logoImageUrl":{"anyOf":[{"type":"string"},{"type":"null"}]},"isPublic":{"type":"boolean"},"creator":{"anyOf":[{"type":"object","properties":{"id":{"type":"string"},"name":{"anyOf":[{"type":"string"},{"type":"null"}]},"image":{"anyOf":[{"type":"string"},{"type":"null"}]},"email":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","image","email"],"additionalProperties":false},{"type":"null"}]},"isHome":{"type":"boolean"},"isMobileHome":{"type":"boolean"},"userPermissions":{"type":"array","items":{"type":"object","properties":{"permission":{"type":"string","enum":["view","modify","full"]}},"required":["permission"],"additionalProperties":false}},"groupPermissions":{"type":"array","items":{"type":"object","properties":{"permission":{"type":"string","enum":["view","modify","full"]}},"required":["permission"],"additionalProperties":false}}},"required":["id","name","logoImageUrl","isPublic","creator","isHome","isMobileHome","userPermissions","groupPermissions"],"additionalProperties":false}}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}},"post":{"operationId":"boardRouter-createBoard","tags":["boards"],"security":[{"apikey":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[A-Za-z0-9-_]*$"},"columnCount":{"type":"number","minimum":1,"maximum":24},"isPublic":{"type":"boolean"}},"required":["name","columnCount","isPublic"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"boardId":{"type":"string"}},"required":["boardId"],"additionalProperties":false}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/boards/{id}/duplicate":{"post":{"operationId":"boardRouter-duplicateBoard","tags":["boards"],"security":[{"apikey":[]}],"parameters":[{"in":"path","name":"id","schema":{"type":"string"},"required":true}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[A-Za-z0-9-_]*$"}},"required":["name"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"boardId":{"type":"string"}},"required":["boardId"],"additionalProperties":false}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/boards/{id}/name":{"patch":{"operationId":"boardRouter-renameBoard","tags":["boards"],"security":[{"apikey":[]}],"parameters":[{"in":"path","name":"id","schema":{"type":"string"},"required":true}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[A-Za-z0-9-_]*$"}},"required":["name"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/boards/{id}/visibility":{"patch":{"operationId":"boardRouter-changeBoardVisibility","tags":["boards"],"security":[{"apikey":[]}],"parameters":[{"in":"path","name":"id","schema":{"type":"string"},"required":true}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"visibility":{"type":"string","enum":["public","private"]}},"required":["visibility"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/boards/{id}":{"delete":{"operationId":"boardRouter-deleteBoard","tags":["boards"],"security":[{"apikey":[]}],"parameters":[{"in":"path","name":"id","schema":{"type":"string"},"required":true}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/boards/{id}/home":{"patch":{"operationId":"boardRouter-setHomeBoard","tags":["boards"],"security":[{"apikey":[]}],"parameters":[{"in":"path","name":"id","schema":{"type":"string"},"required":true}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/boards/{id}/mobile-home":{"patch":{"operationId":"boardRouter-setMobileHomeBoard","tags":["boards"],"security":[{"apikey":[]}],"parameters":[{"in":"path","name":"id","schema":{"type":"string"},"required":true}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/boards/{id}/settings":{"patch":{"operationId":"boardRouter-savePartialBoardSettings","tags":["boards"],"security":[{"apikey":[]}],"parameters":[{"in":"path","name":"id","schema":{"type":"string"},"required":true}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"pageTitle":{"anyOf":[{"type":"string"},{"type":"null"}]},"metaTitle":{"anyOf":[{"type":"string"},{"type":"null"}]},"logoImageUrl":{"anyOf":[{"type":"string"},{"type":"null"}]},"faviconImageUrl":{"anyOf":[{"type":"string"},{"type":"null"}]},"backgroundImageUrl":{"anyOf":[{"type":"string"},{"type":"null"}]},"backgroundImageAttachment":{"type":"string","enum":["fixed","scroll"]},"backgroundImageRepeat":{"type":"string","enum":["repeat","repeat-x","repeat-y","no-repeat"]},"backgroundImageSize":{"type":"string","enum":["cover","contain"]},"primaryColor":{"type":"string","pattern":"^#[0-9A-Fa-f]{6}$"},"secondaryColor":{"type":"string","pattern":"^#[0-9A-Fa-f]{6}$"},"opacity":{"type":"number","minimum":0,"maximum":100},"customCss":{"type":"string","maxLength":16384},"iconColor":{"anyOf":[{"anyOf":[{"type":"string","pattern":"^#[0-9A-Fa-f]{6}$"},{"type":"string","const":""}]},{"type":"null"}]},"itemRadius":{"anyOf":[{"type":"string","const":"xs"},{"type":"string","const":"sm"},{"type":"string","const":"md"},{"type":"string","const":"lg"},{"type":"string","const":"xl"}]},"disableStatus":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/boards/items":{"post":{"operationId":"boardRouter-addItem","tags":["boards"],"security":[{"apikey":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"boardId":{"type":"string"},"kind":{"type":"string","enum":["clock","weather","app","iframe","video","notebook","anchorNote","dnsHoleSummary","dnsHoleControls","smartHome-entityState","smartHome-executeAutomation","stockPrice","mediaServer","calendar","downloads","mediaRequests-requestList","mediaRequests-requestStats","mediaTranscoding","minecraftServerStatus","networkControllerSummary","networkControllerStatus","rssFeed","bookmarks","indexerManager","healthMonitoring","releases","mediaReleases","dockerContainers","firewall","notifications","systemResources","coolify","systemDisks","timetable","immich-serverStats","immich-albumCarousel","paperlessNgx","tracearr","speedtestTracker","uptimeKuma","audioStats","umami","vpn","archiveTeamWarrior","ups","beszelSystemTable","beszelSystemGrid","beszelAlerts","beszelSystemStats","customApi"]},"options":{"default":{},"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"integrationIds":{"default":[],"type":"array","items":{"type":"string"}}},"required":["boardId","kind"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"itemId":{"type":"string"}},"required":["itemId"],"additionalProperties":false}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/info":{"get":{"operationId":"infoRouter-getInfo","tags":["info"],"security":[{"apikey":[]}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"version":{"type":"string"}},"required":["version"],"additionalProperties":false}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/invites":{"get":{"operationId":"inviteRouter-getAll","tags":["invites"],"security":[{"apikey":[]}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"expirationDate":{"type":"string"},"creator":{"type":"object","properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}]},"id":{"type":"string"}},"required":["name","id"],"additionalProperties":false}},"required":["id","expirationDate","creator"],"additionalProperties":false}}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}},"post":{"operationId":"inviteRouter-createInvite","tags":["invites"],"security":[{"apikey":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"expirationDate":{"type":"string"}},"required":["expirationDate"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"token":{"type":"string"}},"required":["id","token"],"additionalProperties":false}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/invites/{id}":{"delete":{"operationId":"inviteRouter-deleteInvite","tags":["invites"],"security":[{"apikey":[]}],"parameters":[{"in":"path","name":"id","schema":{"type":"string"},"required":true}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"not":{}}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/settings/board":{"get":{"operationId":"serverSettingsRouter-getBoardSettings","tags":["settings"],"security":[{"apikey":[]}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"homeBoardId":{"anyOf":[{"type":"string"},{"type":"null"}]},"mobileHomeBoardId":{"anyOf":[{"type":"string"},{"type":"null"}]},"enableStatusByDefault":{"type":"boolean"},"forceDisableStatus":{"type":"boolean"}},"required":["homeBoardId","mobileHomeBoardId","enableStatusByDefault","forceDisableStatus"],"additionalProperties":false}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}},"patch":{"operationId":"serverSettingsRouter-updateBoardSettings","tags":["settings"],"security":[{"apikey":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"homeBoardId":{"anyOf":[{"type":"string"},{"type":"null"}]},"mobileHomeBoardId":{"anyOf":[{"type":"string"},{"type":"null"}]},"enableStatusByDefault":{"type":"boolean"},"forceDisableStatus":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"homeBoardId":{"anyOf":[{"type":"string"},{"type":"null"}]},"mobileHomeBoardId":{"anyOf":[{"type":"string"},{"type":"null"}]},"enableStatusByDefault":{"type":"boolean"},"forceDisableStatus":{"type":"boolean"}},"required":["homeBoardId","mobileHomeBoardId","enableStatusByDefault","forceDisableStatus"],"additionalProperties":false}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/users":{"post":{"operationId":"userRouter-create","tags":["users"],"security":[{"apikey":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"username":{"type":"string","minLength":3,"maxLength":255},"password":{"type":"string","minLength":8,"maxLength":255},"confirmPassword":{"type":"string"},"email":{"anyOf":[{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"string","minLength":0,"maxLength":0}]},"groupIds":{"type":"array","items":{"type":"string"}}},"required":["username","password","confirmPassword","groupIds"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}},"get":{"operationId":"userRouter-getAll","tags":["users"],"security":[{"apikey":[]}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"anyOf":[{"type":"string"},{"type":"null"}]},"email":{"anyOf":[{"type":"string"},{"type":"null"}]},"emailVerified":{"anyOf":[{"type":"string"},{"type":"null"}]},"image":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","email","emailVerified","image"],"additionalProperties":false}}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/users/profileImage":{"put":{"operationId":"userRouter-setProfileImage","tags":["users"],"security":[{"apikey":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string"},"image":{"anyOf":[{"type":"string","maxLength":350000,"pattern":"^data:image\\/(png|jpeg|gif|webp);base64,[A-Za-z0-9/+]+=*$"},{"type":"null"}]}},"required":["userId","image"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/users/selectable":{"get":{"operationId":"userRouter-selectable","tags":["users"],"security":[{"apikey":[]}],"parameters":[{"in":"query","name":"providers","schema":{"type":"array","items":{"type":"string","enum":["credentials","oidc","ldap"]}}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"anyOf":[{"type":"string"},{"type":"null"}]},"image":{"anyOf":[{"type":"string"},{"type":"null"}]},"email":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","image","email"],"additionalProperties":false}}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/users/search":{"post":{"operationId":"userRouter-search","tags":["users"],"security":[{"apikey":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string"},"limit":{"default":10,"type":"number","minimum":1,"maximum":100}},"required":["query"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"anyOf":[{"type":"string"},{"type":"null"}]},"image":{"anyOf":[{"type":"string"},{"type":"null"}]},"email":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","image","email"],"additionalProperties":false}}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/users/{userId}":{"get":{"operationId":"userRouter-getById","tags":["users"],"security":[{"apikey":[]}],"parameters":[{"in":"path","name":"userId","schema":{"type":"string"},"required":true}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"anyOf":[{"type":"string"},{"type":"null"}]},"email":{"anyOf":[{"type":"string"},{"type":"null"}]},"emailVerified":{"anyOf":[{"type":"string"},{"type":"null"}]},"image":{"anyOf":[{"type":"string"},{"type":"null"}]},"provider":{"type":"string"},"homeBoardId":{"anyOf":[{"type":"string"},{"type":"null"}]},"mobileHomeBoardId":{"anyOf":[{"type":"string"},{"type":"null"}]},"firstDayOfWeek":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"pingIconsEnabled":{"type":"boolean"},"defaultSearchEngineId":{"anyOf":[{"type":"string"},{"type":"null"}]},"openSearchInNewTab":{"type":"boolean"},"ddgBangs":{"type":"boolean"},"completedManageTour":{"type":"boolean"},"completedBoardTour":{"type":"boolean"}},"required":["id","name","email","emailVerified","image","provider","homeBoardId","mobileHomeBoardId","firstDayOfWeek","pingIconsEnabled","defaultSearchEngineId","openSearchInNewTab","ddgBangs","completedManageTour","completedBoardTour"],"additionalProperties":false}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}},"delete":{"operationId":"userRouter-delete","tags":["users"],"security":[{"apikey":[]}],"parameters":[{"in":"path","name":"userId","schema":{"type":"string"},"required":true}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/users/profile":{"put":{"operationId":"userRouter-editProfile","tags":["users"],"security":[{"apikey":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","minLength":3,"maxLength":255},"email":{"anyOf":[{"anyOf":[{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"string","const":""}]},{"type":"null"}]}},"required":["id","name"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/users/{userId}/changePassword":{"patch":{"operationId":"userRouter-changePassword","tags":["users"],"security":[{"apikey":[]}],"parameters":[{"in":"path","name":"userId","schema":{"type":"string"},"required":true}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"previousPassword":{"type":"string","minLength":1},"password":{"type":"string","minLength":8,"maxLength":255},"confirmPassword":{"type":"string"}},"required":["previousPassword","password","confirmPassword"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/users/changeHome":{"patch":{"operationId":"userRouter-changeHomeBoards","tags":["users"],"security":[{"apikey":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"homeBoardId":{"anyOf":[{"type":"string"},{"type":"null"}]},"mobileHomeBoardId":{"anyOf":[{"type":"string"},{"type":"null"}]},"userId":{"type":"string"}},"required":["homeBoardId","mobileHomeBoardId","userId"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/users/changeSearchEngine":{"patch":{"operationId":"userRouter-changeDefaultSearchEngine","tags":["users"],"security":[{"apikey":[]}],"deprecated":true,"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"defaultSearchEngineId":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}]},"ddgBangsEnabled":{"type":"boolean"},"userId":{"type":"string"}},"required":["defaultSearchEngineId","ddgBangsEnabled","userId"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/users/search-preferences":{"patch":{"operationId":"userRouter-changeSearchPreferences","tags":["users"],"security":[{"apikey":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"defaultSearchEngineId":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}]},"openInNewTab":{"type":"boolean"},"ddgBangsEnabled":{"type":"boolean"},"userId":{"type":"string"}},"required":["defaultSearchEngineId","openInNewTab","ddgBangsEnabled","userId"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/users/changeScheme":{"patch":{"operationId":"userRouter-changeColorScheme","tags":["users"],"security":[{"apikey":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"colorScheme":{"type":"string","enum":["auto","light","dark"]}},"required":["colorScheme"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/users/ddg-bangs":{"patch":{"operationId":"userRouter-changeDdgBangs","tags":["users"],"security":[{"apikey":[]}],"deprecated":true,"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ddgBangs":{"type":"boolean"},"id":{"type":"string"}},"required":["ddgBangs","id"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/users/firstDayOfWeek":{"patch":{"operationId":"userRouter-changeFirstDayOfWeek","tags":["users"],"security":[{"apikey":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"firstDayOfWeek":{"type":"number","minimum":0,"maximum":6},"id":{"type":"string"}},"required":["firstDayOfWeek","id"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}}},"components":{"schemas":{"error.BAD_REQUEST":{"type":"object","properties":{"message":{"type":"string","description":"The error message","example":"Invalid input data"},"code":{"type":"string","description":"The error code","example":"BAD_REQUEST"},"issues":{"description":"An array of issues that were responsible for the error","example":[],"type":"array","items":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false}}},"required":["message","code"],"additionalProperties":false,"title":"Invalid input data error (400)","description":"The error information","example":{"code":"BAD_REQUEST","message":"Invalid input data","issues":[]}},"error.UNAUTHORIZED":{"type":"object","properties":{"message":{"type":"string","description":"The error message","example":"Authorization not provided"},"code":{"type":"string","description":"The error code","example":"UNAUTHORIZED"},"issues":{"description":"An array of issues that were responsible for the error","example":[],"type":"array","items":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false}}},"required":["message","code"],"additionalProperties":false,"title":"Authorization not provided error (401)","description":"The error information","example":{"code":"UNAUTHORIZED","message":"Authorization not provided","issues":[]}},"error.FORBIDDEN":{"type":"object","properties":{"message":{"type":"string","description":"The error message","example":"Insufficient access"},"code":{"type":"string","description":"The error code","example":"FORBIDDEN"},"issues":{"description":"An array of issues that were responsible for the error","example":[],"type":"array","items":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false}}},"required":["message","code"],"additionalProperties":false,"title":"Insufficient access error (403)","description":"The error information","example":{"code":"FORBIDDEN","message":"Insufficient access","issues":[]}},"error.NOT_FOUND":{"type":"object","properties":{"message":{"type":"string","description":"The error message","example":"Not found"},"code":{"type":"string","description":"The error code","example":"NOT_FOUND"},"issues":{"description":"An array of issues that were responsible for the error","example":[],"type":"array","items":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false}}},"required":["message","code"],"additionalProperties":false,"title":"Not found error (404)","description":"The error information","example":{"code":"NOT_FOUND","message":"Not found","issues":[]}},"error.INTERNAL_SERVER_ERROR":{"type":"object","properties":{"message":{"type":"string","description":"The error message","example":"Internal server error"},"code":{"type":"string","description":"The error code","example":"INTERNAL_SERVER_ERROR"},"issues":{"description":"An array of issues that were responsible for the error","example":[],"type":"array","items":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false}}},"required":["message","code"],"additionalProperties":false,"title":"Internal server error error (500)","description":"The error information","example":{"code":"INTERNAL_SERVER_ERROR","message":"Internal server error","issues":[]}}},"securitySchemes":{"apikey":{"type":"apiKey","name":"ApiKey","description":"API key which can be obtained in the Homarr administration dashboard","in":"header"}}}} \ No newline at end of file diff --git a/apps/docs/tsconfig.json b/apps/docs/tsconfig.json index b723161aa..1142ed3fb 100644 --- a/apps/docs/tsconfig.json +++ b/apps/docs/tsconfig.json @@ -1,9 +1,6 @@ { - "extends": "@docusaurus/tsconfig", "compilerOptions": { - "ignoreDeprecations": "6.0", - "baseUrl": ".", - "target": "es5", + "target": "ES2022", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, @@ -12,10 +9,14 @@ "noEmit": true, "esModuleInterop": true, "module": "esnext", + "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, + "paths": { + "@site/*": ["./*"] + }, }, "include": [ "**/*.ts", diff --git a/apps/nextjs/next.config.ts b/apps/nextjs/next.config.ts index 96f5b743a..4ff31b9aa 100644 --- a/apps/nextjs/next.config.ts +++ b/apps/nextjs/next.config.ts @@ -5,21 +5,9 @@ import "@homarr/common/env"; import "@homarr/core/infrastructure/logs/env"; import "@homarr/docker/env"; -import { readFileSync } from "fs"; - import type { NextConfig } from "next"; import createNextIntlPlugin from "next-intl/plugin"; -const homarrVersion = (() => { - for (const candidate of ["../../package.json", "./package.json"]) { - try { - const pkg = JSON.parse(readFileSync(candidate, "utf-8")); - if (pkg.name === "homarr") return pkg.version as string; - } catch {} - } - return "unknown"; -})(); - // Package path does not work... so we need to use relative path const withNextIntl = createNextIntlPlugin({ requestConfig: "../../packages/translation/src/request.ts", @@ -27,12 +15,10 @@ const withNextIntl = createNextIntlPlugin({ const nextConfig: NextConfig = { env: { - HOMARR_VERSION: homarrVersion, + HOMARR_VERSION: process.env.HOMARR_VERSION ?? "unknown", }, output: "standalone", reactStrictMode: true, - // Ship browser source maps so production client stack traces are readable, see https://github.com/homarr-labs/homarr/issues/5891 - productionBrowserSourceMaps: true, // react compiler breaks mantine-react-table, so disabled for now //reactCompiler: true, /** We already do typechecking as separate tasks in CI */ @@ -43,13 +29,21 @@ const nextConfig: NextConfig = { */ serverExternalPackages: ["dockerode", "isomorphic-dompurify", "jsdom", "better-sqlite3"], experimental: { - // Ship server source maps so production Node stack traces in logs reference original source, see https://github.com/homarr-labs/homarr/issues/5891 - serverSourceMaps: true, optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"], - turbopackFileSystemCacheForDev: false, - preloadEntriesOnStart: false, + turbopackFileSystemCacheForDev: true, webpackMemoryOptimizations: true, }, + turbopack: { + // ponytail: known Turbopack NFT warning from path.join(process.cwd(), …) in + // src/app/api/backup/{route,shared}.ts. No working placement in 16.2.x + // (see vercel/next.js#95125). Suppress until upstream fix lands. + ignoreIssue: [ + { + path: "**/*", + title: "Encountered unexpected file in NFT list", + }, + ], + }, transpilePackages: ["@homarr/ui", "@homarr/notifications", "@homarr/modals", "@homarr/spotlight", "@homarr/widgets"], images: { localPatterns: [ diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index cfea600e7..4a3e0dd3e 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "scripts": { - "build": "pnpm with-env next build", + "build": "HOMARR_VERSION=$(node -p \"require('../../package.json').version\") pnpm with-env next build", "clean": "git clean -xdf .next .turbo node_modules", "dev": "pnpm with-env next dev", "format": "oxfmt .", @@ -59,10 +59,11 @@ "@modelcontextprotocol/sdk": "catalog:", "@scalar/api-reference-react": "catalog:", "@tabler/icons-react": "catalog:", - "@tanstack/query-persist-client-core": "catalog:", + "@tanstack/query-async-storage-persister": "catalog:", "@tanstack/react-query": "catalog:", "@tanstack/react-query-devtools": "catalog:", "@tanstack/react-query-next-experimental": "catalog:", + "@tanstack/react-query-persist-client": "catalog:", "@trpc/client": "catalog:", "@trpc/next": "catalog:", "@trpc/react-query": "catalog:", diff --git a/apps/nextjs/src/app/[locale]/_client-providers/query-cache-persister.ts b/apps/nextjs/src/app/[locale]/_client-providers/query-cache-persister.ts index 70d8581c7..3af24c508 100644 --- a/apps/nextjs/src/app/[locale]/_client-providers/query-cache-persister.ts +++ b/apps/nextjs/src/app/[locale]/_client-providers/query-cache-persister.ts @@ -1,49 +1,32 @@ -import type { AsyncStorage, PersistedQuery } from "@tanstack/query-persist-client-core"; -import { experimental_createQueryPersister } from "@tanstack/query-persist-client-core"; -import { parse, stringify } from "superjson"; +import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister"; +import superjson from "superjson"; import { fetchApi } from "@homarr/api/client"; -import { - getActiveQueryCacheBoardId, - isPersistableWidgetQueryKey, - queryCacheStoragePrefix, -} from "@homarr/api/query-cache"; +import { getActiveQueryCacheBoardId, queryCacheStoragePrefix } from "@homarr/api/query-cache"; const queryCacheStorage = { - async getItem(_key) { - return null; - }, - async setItem(key, value) { + getItem: (_key: string) => null as string | null, + async setItem(_key: string, value: string) { const boardId = getActiveQueryCacheBoardId(); if (!boardId) return; - - try { - await fetchApi.queryCache.setItem.mutate({ boardId, key, value }); - } catch { - // Query persistence is best-effort; failed writes should not affect widget rendering. - } + await fetchApi.queryCache.setItem.mutate({ boardId, value }).catch((error) => { + console.warn("[query-cache] persist failed", error); + }); }, - async removeItem(key) { + async removeItem(_key: string) { const boardId = getActiveQueryCacheBoardId(); if (!boardId) return; - - try { - await fetchApi.queryCache.removeItem.mutate({ boardId, key }); - } catch { - // Query persistence is best-effort; failed removals should not affect widget rendering. - } + await fetchApi.queryCache.removeItem.mutate({ boardId }).catch((error) => { + console.warn("[query-cache] remove failed", error); + }); }, -} satisfies AsyncStorage; +}; export const createWidgetQueryPersister = () => - experimental_createQueryPersister({ + createAsyncStoragePersister({ storage: typeof window === "undefined" ? undefined : queryCacheStorage, - maxAge: Number.POSITIVE_INFINITY, - prefix: queryCacheStoragePrefix, - refetchOnRestore: true, - serialize: (query) => stringify(query), - deserialize: (value) => parse(value), - filters: { - predicate: (query) => isPersistableWidgetQueryKey(query.queryKey), - }, + key: queryCacheStoragePrefix, + throttleTime: 2000, + serialize: (data) => superjson.stringify(data), + deserialize: (data) => superjson.parse(data), }); diff --git a/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx b/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx index d09acd8c8..7d06a8951 100644 --- a/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx +++ b/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx @@ -2,13 +2,15 @@ import type { PropsWithChildren } from "react"; import { useState } from "react"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { QueryClient } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental"; +import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client"; import { createWSClient, httpBatchStreamLink, httpLink, + httpSubscriptionLink, isNonJsonSerializable, loggerLink, splitLink, @@ -21,7 +23,13 @@ import { TRPCClientError } from "@trpc/client"; import type { AppRouter } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; -import { queryCacheDefaultStaleTimeMs } from "@homarr/api/query-cache"; +import { + isPersistableWidgetQueryKey, + queryCacheBuster, + queryCacheDefaultGcTimeMs, + queryCacheDefaultRefetchIntervalMs, + queryCacheDefaultStaleTimeMs, +} from "@homarr/api/query-cache"; import { createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/shared"; import { env } from "@homarr/common/env"; import { showWarningNotification } from "@homarr/notifications"; @@ -29,7 +37,6 @@ import { showWarningNotification } from "@homarr/notifications"; import { createWidgetQueryPersister } from "./query-cache-persister"; const getWebSocketProtocol = () => { - // window is not defined on server side if (typeof window === "undefined") { return "ws"; } @@ -55,32 +62,36 @@ const wsClient = createWSClient({ }); export function TRPCReactProvider(props: PropsWithChildren) { - const [queryPersister] = useState(() => createWidgetQueryPersister()); - const [queryClient] = useState( - () => - new QueryClient({ - defaultOptions: { - queries: { - persister: queryPersister.persisterFn, - staleTime: queryCacheDefaultStaleTimeMs, - }, - mutations: { - onError(error) { - if ( - error instanceof TRPCClientError && - error.data?.code === "FORBIDDEN" && - error.message === "Mutations are disabled in demo mode" - ) { - showWarningNotification({ - title: "Demo mode", - message: "This action is disabled in demo mode.", - }); - } - }, + const [persister] = useState(() => createWidgetQueryPersister()); + const [queryClient] = useState(() => { + const client = new QueryClient({ + defaultOptions: { + queries: { + staleTime: queryCacheDefaultStaleTimeMs, + gcTime: queryCacheDefaultGcTimeMs, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + retry: 3, + }, + mutations: { + onError(error) { + if ( + error instanceof TRPCClientError && + error.data?.code === "FORBIDDEN" && + error.message === "Mutations are disabled in demo mode" + ) { + showWarningNotification({ + title: "Demo mode", + message: "This action is disabled in demo mode.", + }); + } }, }, - }), - ); + }, + }); + client.setQueryDefaults([["widget"]], { refetchInterval: queryCacheDefaultRefetchIntervalMs }); + return client; + }); const [trpcClient] = useState(() => { return clientApi.createClient({ @@ -91,16 +102,21 @@ export function TRPCReactProvider(props: PropsWithChildren) { }), splitLink({ condition: ({ type }) => type === "subscription", - true: wsLink({ - client: wsClient, - transformer: superjson, + true: splitLink({ + condition: ({ path }) => path === "widget.beszel.subscribeSystemStats", + true: httpSubscriptionLink({ + url: getTrpcUrl(), + transformer: superjson, + eventSourceOptions: { withCredentials: true }, + }), + false: wsLink({ + client: wsClient, + transformer: superjson, + }), }), false: splitLink({ condition: ({ input }) => isNonJsonSerializable(input), true: httpLink({ - /** - * We don't want to transform the data here as we want to use form data - */ transformer: { serialize(object: unknown) { return object; @@ -115,7 +131,7 @@ export function TRPCReactProvider(props: PropsWithChildren) { false: httpBatchStreamLink({ transformer: superjson, url: getTrpcUrl(), - maxURLLength: 2083, // Suggested by tRPC: https://trpc.io/docs/client/links/httpBatchLink#setting-a-maximum-url-length + maxURLLength: 2083, headers: createHeadersCallbackForSource("nextjs-react (json)"), }), }), @@ -126,10 +142,21 @@ export function TRPCReactProvider(props: PropsWithChildren) { return ( - + + query.state.status === "success" && isPersistableWidgetQueryKey(query.queryKey), + }, + }} + > {props.children} - + ); } diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx index 2144304c5..16be5c478 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx @@ -5,10 +5,10 @@ import { TRPCError } from "@trpc/server"; // Placed here because gridstack styles are used for board content import "~/styles/gridstack.scss"; -import type { DehydratedState } from "@tanstack/react-query"; import { dehydrate, HydrationBoundary } from "@tanstack/react-query"; -import { parse } from "superjson"; - +import type { PersistedClient } from "@tanstack/react-query-persist-client"; +import superjson from "superjson"; +import { queryCacheBuster } from "@homarr/api/query-cache"; import { getQueryClient } from "@homarr/api/server"; import { IntegrationProvider } from "@homarr/auth/client"; import { auth } from "@homarr/auth/next"; @@ -17,11 +17,12 @@ import { isNullOrWhitespace } from "@homarr/common"; import { createLogger } from "@homarr/core/infrastructure/logs"; import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; import type { WidgetKind } from "@homarr/definitions"; -import { getAllQueryCacheAsync } from "@homarr/redis"; +import { getQueryCacheAsync } from "@homarr/redis"; import { getI18n } from "@homarr/translation/server"; import { prefetchForKindAsync } from "@homarr/widgets/prefetch"; import { createMetaTitle } from "~/metadata"; +import { env } from "~/env"; import { createBoardLayout } from "../_layout-creator"; import type { Board, Item } from "../_types"; import { DynamicClientBoard } from "./_dynamic-client"; @@ -40,7 +41,7 @@ export const createBoardContentPage = >( }: Props) => { return { layout: createBoardLayout({ - headerActions: , + headerActions: , getInitialBoardAsync: getInitialBoard, withTour: true, }), @@ -79,14 +80,16 @@ export const createBoardContentPage = >( const userId = session?.user.id ?? "anonymous"; return ( - - - - - - - - + <> + + + + + + + + + ); }, generateMetadataAsync: async ({ params }: { params: Promise }): Promise => { @@ -118,36 +121,18 @@ export const createBoardContentPage = >( }; }; -interface PersistedEntry { - state: { data: unknown; dataUpdatedAt: number; status: string }; - queryKey: unknown[]; - queryHash: string; -} - async function QueryCacheHydration({ userId, boardId }: { userId: string; boardId: string }) { try { - const entries = await getAllQueryCacheAsync(userId, boardId); - const queries: DehydratedState["queries"] = []; + const serialized = await getQueryCacheAsync(userId, boardId); + if (!serialized) return null; - for (const serialized of Object.values(entries)) { - try { - const persisted = parse(serialized); - if (persisted.state.status !== "success") continue; + const persisted = superjson.parse(serialized); + if (!persisted || persisted.buster !== queryCacheBuster) return null; + if (!persisted.clientState?.queries?.length) return null; - queries.push({ - state: persisted.state as DehydratedState["queries"][number]["state"], - queryKey: persisted.queryKey, - queryHash: persisted.queryHash, - }); - } catch { - // skip malformed entries - } - } - - if (queries.length === 0) return null; - - return ; - } catch { + return ; + } catch (error) { + logger.warn(new ErrorWithMetadata("Failed to hydrate query cache", { userId, boardId }, { cause: error })); return null; } } diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx index cc3868e50..30c91de40 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx @@ -41,13 +41,12 @@ import { useDynamicSectionActions } from "~/components/board/sections/dynamic/dy import { IntegrationSelectModal } from "~/components/integration/integration-select-modal"; import { HeaderButton } from "~/components/layout/header/button"; -export const BoardContentHeaderActions = () => { +export const BoardContentHeaderActions = ({ demoReadOnly }: { demoReadOnly: boolean }) => { const [isEditMode] = useEditMode(); const board = useRequiredBoard(); const { hasChangeAccess } = useBoardPermissions(board); - const { data: demoMode, isLoading } = clientApi.info.isDemoMode.useQuery(); - if (!hasChangeAccess || isLoading) { + if (!hasChangeAccess) { return ; } @@ -55,9 +54,9 @@ export const BoardContentHeaderActions = () => { <> {isEditMode && } - + - {!demoMode && ( + {!demoReadOnly && ( @@ -158,7 +157,7 @@ const AddMenu = () => { ); }; -const EditModeMenu = ({ demoMode }: { demoMode: boolean }) => { +const EditModeMenu = ({ demoReadOnly }: { demoReadOnly: boolean }) => { const [isEditMode, { open, close }] = useEditMode(); const board = useRequiredBoard(); const utils = clientApi.useUtils(); @@ -188,11 +187,11 @@ const EditModeMenu = ({ demoMode }: { demoMode: boolean }) => { const toggle = useCallback(() => { if (isEditMode) { - if (demoMode) return discardDemoChanges(); + if (demoReadOnly) return discardDemoChanges(); return saveBoard(board); } open(); - }, [board, isEditMode, demoMode, saveBoard, open, discardDemoChanges]); + }, [board, isEditMode, demoReadOnly, saveBoard, open, discardDemoChanges]); useHotkeys([[hotkeys.toggleBoardEdit, toggle]]); usePreventLeaveWithDirty(isEditMode); diff --git a/apps/nextjs/src/app/[locale]/layout.tsx b/apps/nextjs/src/app/[locale]/layout.tsx index fdae0741a..c1790ee2f 100644 --- a/apps/nextjs/src/app/[locale]/layout.tsx +++ b/apps/nextjs/src/app/[locale]/layout.tsx @@ -143,7 +143,7 @@ export default async function Layout(props: { - + diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/_components/secrets/integration-secret-icons.ts b/apps/nextjs/src/app/[locale]/manage/integrations/_components/secrets/integration-secret-icons.ts index 18b32206e..1b2ed3c7b 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/_components/secrets/integration-secret-icons.ts +++ b/apps/nextjs/src/app/[locale]/manage/integrations/_components/secrets/integration-secret-icons.ts @@ -26,6 +26,8 @@ export const integrationSecretIcons = { url: IconLink, opnsenseApiKey: IconKey, opnsenseApiSecret: IconPassword, + patchmonApiKey: IconKey, + patchmonApiSecret: IconPassword, githubAppId: IconCode, githubInstallationId: IconPlug, privateKey: IconKey, diff --git a/apps/nextjs/src/app/[locale]/manage/tools/api/page.tsx b/apps/nextjs/src/app/[locale]/manage/tools/api/page.tsx index bdd86cc7c..db8050ff1 100644 --- a/apps/nextjs/src/app/[locale]/manage/tools/api/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/tools/api/page.tsx @@ -10,6 +10,7 @@ import { extractBaseUrlFromHeaders } from "@homarr/common"; import { getScopedI18n } from "@homarr/translation/server"; import { createMetaTitle } from "~/metadata"; +import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb"; import { extractMcpTools } from "~/app/api/mcp/_extract-tools"; import { ApiKeysManagement } from "./components/api-keys"; import { ApiPageTabs } from "./components/api-page-tabs"; @@ -79,17 +80,20 @@ export default async function ApiPage() { const toolGroups = getMcpToolGroups(); return ( - - } - authenticationPanel={} - baseUrl={baseUrl} - hasApiKeys={apiKeys.length > 0} - toolGroups={toolGroups} - /> - + <> + + + } + authenticationPanel={} + baseUrl={baseUrl} + hasApiKeys={apiKeys.length > 0} + toolGroups={toolGroups} + /> + + ); } diff --git a/apps/nextjs/src/app/[locale]/manage/tools/backup/page.tsx b/apps/nextjs/src/app/[locale]/manage/tools/backup/page.tsx index 80a794b57..037c11677 100644 --- a/apps/nextjs/src/app/[locale]/manage/tools/backup/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/tools/backup/page.tsx @@ -6,6 +6,7 @@ import { dbEnv } from "@homarr/core/infrastructure/db/env"; import { getScopedI18n } from "@homarr/translation/server"; import { createMetaTitle } from "~/metadata"; +import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb"; import { BackupExportCard } from "./_components/backup-export-card"; import { BackupImportCard } from "./_components/backup-import-card"; @@ -31,10 +32,13 @@ export default async function BackupPage() { const t = await getScopedI18n("management.page.tool.backup"); return ( - - {t("title")} - - - + <> + + + {t("title")} + + + + ); } diff --git a/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx b/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx index 764f91844..43bbc3ff3 100644 --- a/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx +++ b/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx @@ -1,5 +1,6 @@ "use client"; +import { useCallback } from "react"; import { useRouter } from "next/navigation"; import type { MantineColor } from "@mantine/core"; import { ActionIcon, Avatar, Badge, Box, Button, Group, Menu, Text, Tooltip } from "@mantine/core"; @@ -156,29 +157,26 @@ interface DockerTableProps { export function DockerTable({ initialData }: DockerTableProps) { const t = useI18n(); const tDocker = useScopedI18n("docker"); - const { data, isFetching } = clientApi.docker.getContainers.useQuery(undefined, { + const { data, isFetching, refetch } = clientApi.docker.getContainers.useQuery(undefined, { initialData, refetchOnMount: false, - refetchOnWindowFocus: false, - refetchOnReconnect: false, }); const relativeTime = useTimeAgo(data?.timestamp ?? new Date()); - const utils = clientApi.useUtils(); - const { mutate, isPending } = clientApi.docker.invalidate.useMutation({ - async onSuccess() { - await utils.docker.getContainers.invalidate(); - showSuccessNotification({ - title: tDocker("action.refresh.notification.success.title"), - message: tDocker("action.refresh.notification.success.message"), - }); - }, - onError() { - showErrorNotification({ - title: tDocker("action.refresh.notification.error.title"), - message: tDocker("action.refresh.notification.error.message"), - }); - }, - }); + const mutate = useCallback(() => { + void refetch().then((result) => { + if (result.isError) { + showErrorNotification({ + title: tDocker("action.refresh.notification.error.title"), + message: tDocker("action.refresh.notification.error.message"), + }); + } else { + showSuccessNotification({ + title: tDocker("action.refresh.notification.success.title"), + message: tDocker("action.refresh.notification.success.message"), + }); + } + }); + }, [refetch, tDocker]); const containers = data?.containers ?? []; @@ -207,7 +205,12 @@ export function DockerTable({ initialData }: DockerTableProps) { mantineTableHeadCellProps: { style: { padding: "4px 8px" } }, renderRowActions: ({ row }: { row: MRT_Row }) => , renderTopToolbarCustomActions: () => ( - ), diff --git a/apps/nextjs/src/app/[locale]/manage/users/create/_components/create-user-stepper.tsx b/apps/nextjs/src/app/[locale]/manage/users/create/_components/create-user-stepper.tsx index 3997bda60..6406259f5 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/create/_components/create-user-stepper.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/create/_components/create-user-stepper.tsx @@ -17,6 +17,7 @@ import { showErrorNotification } from "@homarr/notifications"; import { useI18n, useScopedI18n } from "@homarr/translation/client"; import { UserAvatar } from "@homarr/ui"; import { createCustomErrorParams } from "@homarr/validation/form/i18n"; +import { optionalEmailSchema } from "@homarr/validation/email"; import { userPasswordSchema } from "@homarr/validation/user"; import { GroupSelectModal } from "~/components/access/group-select-modal"; @@ -60,7 +61,7 @@ export const UserCreateStepperComponent = ({ initialGroups }: UserCreateStepperC const generalForm = useZodForm( z.object({ username: z.string().min(1), - email: z.string().email().or(z.string().length(0).optional()), + email: optionalEmailSchema, }), { initialValues: { diff --git a/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/layout.tsx b/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/layout.tsx index fd6766417..3c8724068 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/layout.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/layout.tsx @@ -7,6 +7,7 @@ import { getI18n, getScopedI18n } from "@homarr/translation/server"; import { Link } from "@homarr/ui"; import { ManageContainer } from "~/components/manage/manage-container"; +import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb"; import { NavigationLink } from "./_navigation"; interface LayoutProps { @@ -24,6 +25,7 @@ export default async function Layout(props: PropsWithChildren) { return ( + diff --git a/apps/nextjs/src/app/api/backup/shared.ts b/apps/nextjs/src/app/api/backup/shared.ts index 2d93e0bca..4b00a4bb3 100644 --- a/apps/nextjs/src/app/api/backup/shared.ts +++ b/apps/nextjs/src/app/api/backup/shared.ts @@ -1,11 +1,13 @@ import fs from "fs"; import path from "path"; -const migrationsFolderCandidates = [ - path.join(process.cwd(), "db/migrations/sqlite"), - path.join(process.cwd(), "packages/db/migrations/sqlite"), - path.resolve(process.cwd(), "../../db/migrations/sqlite"), - "/app/db/migrations/sqlite", -]; - -export const findMigrationsFolder = () => migrationsFolderCandidates.find((candidate) => fs.existsSync(candidate)); +export const findMigrationsFolder = () => { + const cwd = process.cwd(); + const migrationsFolderCandidates = [ + path.join(cwd, "db/migrations/sqlite"), + path.join(cwd, "packages/db/migrations/sqlite"), + path.resolve(cwd, "../../db/migrations/sqlite"), + "/app/db/migrations/sqlite", + ]; + return migrationsFolderCandidates.find((candidate) => fs.existsSync(candidate)); +}; diff --git a/apps/nextjs/src/components/access/inherit-access-table.tsx b/apps/nextjs/src/components/access/inherit-access-table.tsx index 5b439ce8c..92905fce1 100644 --- a/apps/nextjs/src/components/access/inherit-access-table.tsx +++ b/apps/nextjs/src/components/access/inherit-access-table.tsx @@ -30,7 +30,19 @@ export const InheritAccessTable = ({ - {accessQueryData.inherited.map(({ group, permission }) => { + {Array.from( + accessQueryData.inherited + .reduce((map, entry) => { + const existing = map.get(entry.group.id); + if (!existing) { + map.set(entry.group.id, entry); + } else if (getPermissionsWithChildren([entry.permission]).includes(existing.permission)) { + map.set(entry.group.id, entry); + } + return map; + }, new Map()) + .values(), + ).map(({ group, permission }) => { const entityPermission = permission in mapPermissions ? mapPermissions[permission] diff --git a/apps/nextjs/src/components/board/items/item-content.tsx b/apps/nextjs/src/components/board/items/item-content.tsx index 9f56a7d48..d869538e4 100644 --- a/apps/nextjs/src/components/board/items/item-content.tsx +++ b/apps/nextjs/src/components/board/items/item-content.tsx @@ -1,6 +1,8 @@ +import type { MutableRefObject } from "react"; +import { useRef } from "react"; import { Badge, Card } from "@mantine/core"; import { useElementSize } from "@mantine/hooks"; -import { QueryErrorResetBoundary } from "@tanstack/react-query"; +import { QueryErrorResetBoundary, useQueryClient } from "@tanstack/react-query"; import combineClasses from "clsx"; import { NoIntegrationSelectedError } from "@homarr/widgets/errors/classes"; import { ErrorBoundary } from "react-error-boundary"; @@ -16,6 +18,7 @@ import classes from "../sections/item.module.css"; import { useItemActions } from "./item-actions"; import itemContentClasses from "./item-content.module.css"; import { BoardItemMenu } from "./item-menu"; +import { WidgetContextMenu } from "./widget-context-menu"; interface BoardItemContentProps { item: SectionItem; @@ -30,30 +33,33 @@ const getOverflowFromKind = (kind: SectionItem["kind"]) => { export const BoardItemContent = ({ item }: BoardItemContentProps) => { const { ref, width, height } = useElementSize(); const board = useRequiredBoard(); + const widgetStateRef = useRef | null>(null); return ( <> - - - + + + + + {item.advancedOptions.title?.trim() && ( | null>; } const InnerContent = ({ item, ...dimensions }: InnerContentProps) => { @@ -98,12 +105,16 @@ const InnerContent = ({ item, ...dimensions }: InnerContentProps) => { updateItemOptions({ itemId: item.id, newOptions }); const widgetSupportsIntegrations = "supportedIntegrations" in definition && definition.supportedIntegrations.length >= 1; + const queryClient = useQueryClient(); return ( {({ reset }) => ( { + void queryClient.invalidateQueries({ queryKey: [["widget"]] }); + reset(); + }} fallbackRender={({ resetErrorBoundary, error }) => ( <> diff --git a/apps/nextjs/src/components/board/items/item-menu.tsx b/apps/nextjs/src/components/board/items/item-menu.tsx index 1c0c4100a..b8a136bbb 100644 --- a/apps/nextjs/src/components/board/items/item-menu.tsx +++ b/apps/nextjs/src/components/board/items/item-menu.tsx @@ -85,6 +85,7 @@ const BoardItemMenuInner = ({ offset, item, resetErrorBoundary }: BoardItemMenuP ), integrationSupport: "supportedIntegrations" in currentDefinition, settings, + itemId: item.id, appId: item.kind === "app" ? (item.options.appId as string | undefined) : undefined, }, { diff --git a/apps/nextjs/src/components/board/items/widget-context-menu.tsx b/apps/nextjs/src/components/board/items/widget-context-menu.tsx new file mode 100644 index 000000000..ff225f642 --- /dev/null +++ b/apps/nextjs/src/components/board/items/widget-context-menu.tsx @@ -0,0 +1,306 @@ +"use client"; + +import type { MutableRefObject, ReactNode } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Group, Loader, Menu, Switch, Text } from "@mantine/core"; +import { IconCopy, IconLayoutKanban, IconRefresh, IconSettings, IconTrash } from "@tabler/icons-react"; +import type { QueryClient, QueryKey } from "@tanstack/react-query"; +import { useIsFetching, useQueryClient } from "@tanstack/react-query"; + +import { clientApi } from "@homarr/api/client"; +import { useSession } from "@homarr/auth/client"; +import { useRequiredBoard } from "@homarr/boards/context"; +import { useEditMode } from "@homarr/boards/edit-mode"; +import { useConfirmModal, useModalAction } from "@homarr/modals"; +import { useSettings } from "@homarr/settings"; +import { translateIfNecessary } from "@homarr/translation"; +import { useI18n, useScopedI18n } from "@homarr/translation/client"; +import type { WidgetContextMenuAction } from "@homarr/widgets"; +import { reduceWidgetOptionsWithDefaultValues, widgetImports } from "@homarr/widgets"; +import { WidgetEditModal } from "@homarr/widgets/modals"; + +import type { SectionItem } from "~/app/[locale]/boards/_types"; +import { useSectionContext } from "../sections/section-context"; +import { useItemActions } from "./item-actions"; +import { ItemMoveModal } from "./item-move-modal"; + +interface WidgetContextMenuProps { + item: SectionItem; + widgetStateRef: MutableRefObject | null>; + children: ReactNode; +} + +export const WidgetContextMenu = ({ item, widgetStateRef, children }: WidgetContextMenuProps) => { + const { data: session } = useSession(); + const [isEditMode] = useEditMode(); + const board = useRequiredBoard(); + const tItem = useScopedI18n("item"); + const tMenu = useScopedI18n("item.menu.label"); + const t = useI18n(); + const settings = useSettings(); + const { openModal } = useModalAction(WidgetEditModal); + const { openModal: openMoveModal } = useModalAction(ItemMoveModal); + const { openConfirmModal } = useConfirmModal(); + const { updateItemOptions, updateItemAdvancedOptions, updateItemIntegrations, duplicateItem, removeItem } = + useItemActions(); + const { data: integrationData, isPending } = clientApi.integration.all.useQuery(); + const { mutate: saveBoard } = clientApi.board.saveBoard.useMutation(); + const currentDefinition = useMemo(() => widgetImports[item.kind].definition, [item.kind]); + const { gridstack } = useSectionContext().refs; + const queryClient = useQueryClient(); + + const widgetQueryKey = useMemo( + () => ("queryKey" in currentDefinition ? (currentDefinition.queryKey as QueryKey) : [["widget", item.kind]]), + [currentDefinition, item.kind], + ); + const isWidgetFetching = useIsFetching({ queryKey: widgetQueryKey }) > 0; + const handleRefetch = useCallback(() => { + void queryClient.refetchQueries({ queryKey: widgetQueryKey, type: "all" }); + }, [queryClient, widgetQueryKey]); + + const options = useMemo( + () => reduceWidgetOptionsWithDefaultValues(item.kind, settings, item.options) as Record, + [item.kind, settings, item.options], + ); + + type OptionDef = { type: string; skipContextMenu?: boolean }; + const toggleOptions = useMemo(() => { + const rawOptions = currentDefinition.createOptions(settings) as unknown as Record; + return Object.entries(rawOptions).filter(([, def]) => def.type === "switch" && !def.skipContextMenu); + }, [currentDefinition, settings]); + + const widgetContextActions = useMemo(() => { + const def = currentDefinition as Record; + if (typeof def.contextActions !== "function") return []; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const actions = (def.contextActions as any)({ + options, + setOptions: (partial: Record) => { + updateItemOptions({ itemId: item.id, newOptions: { ...options, ...partial } }); + }, + integrationIds: item.integrationIds, + context: { isEditMode, boardId: board.id, itemId: item.id }, + widgetStateRef, + }); + return (Array.isArray(actions) ? actions : []) as WidgetContextMenuAction[]; + }, [currentDefinition, options, item, updateItemOptions, isEditMode, board.id, widgetStateRef]); + + const persistBoard = useCallback( + (updatedItems: typeof board.items) => { + saveBoard({ ...board, items: updatedItems }); + }, + [board, saveBoard], + ); + + const openEditModal = useCallback(() => { + openModal( + { + kind: item.kind, + value: { + advancedOptions: item.advancedOptions, + options: item.options, + integrationIds: item.integrationIds, + }, + onSuccessfulEdit: (editResult) => { + updateItemOptions({ itemId: item.id, newOptions: editResult.options }); + updateItemAdvancedOptions({ itemId: item.id, newAdvancedOptions: editResult.advancedOptions }); + updateItemIntegrations({ itemId: item.id, newIntegrations: editResult.integrationIds }); + if (!isEditMode) { + persistBoard( + board.items.map((boardItem) => + boardItem.id !== item.id + ? boardItem + : { + ...boardItem, + options: editResult.options, + advancedOptions: editResult.advancedOptions, + integrationIds: editResult.integrationIds, + }, + ), + ); + } + }, + integrationData: (integrationData ?? []).filter( + (integration) => + "supportedIntegrations" in currentDefinition && + (currentDefinition.supportedIntegrations as string[]).some((kind) => kind === integration.kind), + ), + integrationSupport: "supportedIntegrations" in currentDefinition, + settings, + appId: item.kind === "app" ? (item.options.appId as string | undefined) : undefined, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + { title: (fn: any) => `${fn("item.edit.title")} - ${fn(`widget.${item.kind}.name`)}` }, + ); + }, [ + openModal, + item, + updateItemOptions, + updateItemAdvancedOptions, + updateItemIntegrations, + integrationData, + currentDefinition, + settings, + isEditMode, + persistBoard, + board, + ]); + + const handleToggle = useCallback( + (key: string) => (checked: boolean) => { + const newOptions = { ...options, [key]: checked }; + updateItemOptions({ itemId: item.id, newOptions }); + persistBoard( + board.items.map((boardItem) => (boardItem.id !== item.id ? boardItem : { ...boardItem, options: newOptions })), + ); + }, + [options, item.id, updateItemOptions, persistBoard, board], + ); + + if (!session) return <>{children}; + + const visibleWidgetActions = widgetContextActions.filter((a) => !a.hidden); + + return ( + + {children} + + {toggleOptions.length > 0 && ( + <> + {tMenu("options")} + {toggleOptions.map(([key]) => ( + handleToggle(key)(!options[key])}> + {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} + + {String(translateIfNecessary(t, ((fn: any) => fn(`widget.${item.kind}.option.${key}.label`)) as any))} + + + + ))} + + )} + + {visibleWidgetActions.length > 0 && ( + <> + {toggleOptions.length > 0 && } + {tMenu("actions")} + {visibleWidgetActions.map((action) => { + const Icon = action.icon; + return ( + : undefined} + onClick={action.onClick} + disabled={action.disabled} + color={action.color} + > + {String(translateIfNecessary(t, action.label))} + + ); + })} + + )} + + {isEditMode && ( + <> + {(toggleOptions.length > 0 || visibleWidgetActions.length > 0) && } + } + onClick={() => { + if (!gridstack.current) return; + openMoveModal({ item, columnCount: gridstack.current.getColumn(), gridStack: gridstack.current }); + }} + > + {tItem("action.moveResize")} + + } + onClick={() => duplicateItem({ itemId: item.id })} + > + {tItem("action.duplicate")} + + + )} + + <> + {(toggleOptions.length > 0 || visibleWidgetActions.length > 0 || isEditMode) && } + : } + onClick={handleRefetch} + disabled={isWidgetFetching} + > + + {tMenu("refresh")} + + + + + + } + onClick={openEditModal} + disabled={isPending} + > + {tMenu("settings")} + + + + {isEditMode && ( + <> + + } + onClick={() => { + openConfirmModal({ + title: tItem("remove.title"), + children: tItem("remove.message"), + onConfirm: () => removeItem({ itemId: item.id }), + }); + }} + > + {tItem("action.remove")} + + + )} + + + ); +}; + +const WidgetCacheAge = ({ + queryClient, + queryKey, + isFetching, +}: { + queryClient: QueryClient; + queryKey: QueryKey; + isFetching: boolean; +}) => { + // 1s tick for "just now" → "1s ago" label transition; isFetching prop handles refetch reactivity + const [, setTick] = useState(0); + useEffect(() => { + const id = setInterval(() => setTick((n) => n + 1), 1000); + return () => clearInterval(id); + }, []); + + if (isFetching) return ; + + const timestamps = queryClient + .getQueryCache() + .findAll({ queryKey }) + .map((q) => q.state.dataUpdatedAt) + .filter(Boolean); + if (timestamps.length === 0) return null; + + const latest = Math.max(...timestamps); + const seconds = Math.floor((Date.now() - latest) / 1000); + if (seconds < 5) return "just now"; + if (seconds < 60) return `${seconds}s ago`; + return `${Math.floor(seconds / 60)}m ago`; +}; diff --git a/apps/nextjs/src/components/board/sections/item.module.css b/apps/nextjs/src/components/board/sections/item.module.css index f679c62ca..a7168659e 100644 --- a/apps/nextjs/src/components/board/sections/item.module.css +++ b/apps/nextjs/src/components/board/sections/item.module.css @@ -10,3 +10,8 @@ background-color: var(--background-color) !important; border-color: var(--border-color) !important; } + +.itemCard:global(.iframe-wrapper) { + background-color: transparent !important; + border-color: transparent !important; +} diff --git a/apps/nextjs/src/components/integration/integration-grid-shared.ts b/apps/nextjs/src/components/integration/integration-grid-shared.ts index 2fc7228c8..7451af1e7 100644 --- a/apps/nextjs/src/components/integration/integration-grid-shared.ts +++ b/apps/nextjs/src/components/integration/integration-grid-shared.ts @@ -48,10 +48,8 @@ export const categoryTranslationKeys: Record = { search: "integration.category.search", mediaTranscoding: "integration.category.mediaTranscoding", networkController: "integration.category.networkController", - releasesProvider: "integration.category.releasesProvider", notifications: "integration.category.notifications", firewall: "integration.category.firewall", - timetable: "integration.category.timetable", photoService: "integration.category.photoService", notes: "integration.category.notes", mediaMonitoring: "integration.category.mediaMonitoring", diff --git a/apps/nextjs/src/components/layout/header.tsx b/apps/nextjs/src/components/layout/header.tsx index 32ffa81d7..e9d93f7fc 100644 --- a/apps/nextjs/src/components/layout/header.tsx +++ b/apps/nextjs/src/components/layout/header.tsx @@ -18,7 +18,7 @@ interface Props { export const MainHeader = ({ logo, actions, hasNavigation = true }: Props) => { return ( - + {hasNavigation && } diff --git a/apps/nextjs/src/components/navigation/dynamic-breadcrumb.tsx b/apps/nextjs/src/components/navigation/dynamic-breadcrumb.tsx index d200aa966..2ed6bc5f8 100644 --- a/apps/nextjs/src/components/navigation/dynamic-breadcrumb.tsx +++ b/apps/nextjs/src/components/navigation/dynamic-breadcrumb.tsx @@ -14,6 +14,8 @@ interface DynamicBreadcrumbProps { nonInteractable?: string[]; } +const categorySegments = new Set(["tools"]); + /** * Breadcrumb is client side rendered. Elements are automatically * calculated and translated using dynamic keys. @@ -60,8 +62,11 @@ export const DynamicBreadcrumb = ({ const translationKey = `${pathnameParts.slice(0, index + 1).join(".")}`; const mappedValue = dynamicMappings?.get(pathnamePart); + const isNonInteractable = + nonInteractable?.includes(pathnamePart) === true || categorySegments.has(pathnamePart); + if (mappedValue) { - if (nonInteractable?.includes(pathnamePart)) { + if (isNonInteractable) { return {mappedValue}; } return ( @@ -71,7 +76,7 @@ export const DynamicBreadcrumb = ({ ); } - if (nonInteractable?.includes(pathnamePart)) { + if (isNonInteractable) { return {t(`${translationKey}.label` as TranslationKeys)}; } diff --git a/apps/nextjs/src/components/onboarding/manage-tour.tsx b/apps/nextjs/src/components/onboarding/manage-tour.tsx index 70ca61084..c242a1bb2 100644 --- a/apps/nextjs/src/components/onboarding/manage-tour.tsx +++ b/apps/nextjs/src/components/onboarding/manage-tour.tsx @@ -105,7 +105,7 @@ export const ManageTourProvider = ({ children, isAdmin }: ManageTourProviderProp content: ( ), }, diff --git a/apps/nextjs/src/env.ts b/apps/nextjs/src/env.ts index 96f03e7b7..d285a8393 100644 --- a/apps/nextjs/src/env.ts +++ b/apps/nextjs/src/env.ts @@ -4,6 +4,7 @@ export const env = createEnv({ server: { UNSAFE_ENABLE_MOCK_INTEGRATION: createBooleanSchema(false), DEMO_MODE: createBooleanSchema(false), + DEMO_READ_ONLY: createBooleanSchema(true), }, experimental__runtimeEnv: process.env, }); diff --git a/apps/nextjs/src/versions/package-reader.ts b/apps/nextjs/src/versions/package-reader.ts index d62c97b20..bf1cd6405 100644 --- a/apps/nextjs/src/versions/package-reader.ts +++ b/apps/nextjs/src/versions/package-reader.ts @@ -35,14 +35,22 @@ export const getDependenciesAsync = async (): Promise = type DependencyCatalog = Map; const parseDependencyCatalogsAsync = async (): Promise => { const currentDir = path.dirname(fileURLToPath(import.meta.url)); - const workspaceConfigContent = await fsPromises.readFile( - path.join(currentDir, "..", "..", "..", "..", "pnpm-workspace.yaml"), - "utf-8", - ); - const workspaceConfig: unknown = parseYaml(workspaceConfigContent); - const parseResult = workspaceConfigCatalogSchema.parse(workspaceConfig); + const workspaceConfigPath = path.join(currentDir, "..", "..", "..", "..", "pnpm-workspace.yaml"); - return new Map(Object.entries(parseResult.catalog ?? {})); + try { + const workspaceConfigContent = await fsPromises.readFile(workspaceConfigPath, "utf-8"); + const workspaceConfig: unknown = parseYaml(workspaceConfigContent); + const parseResult = workspaceConfigCatalogSchema.parse(workspaceConfig); + return new Map(Object.entries(parseResult.catalog ?? {})); + } catch (error) { + // The workspace config is absent in Docker (only the runner stage is shipped), + // so a missing file is expected. Surface any other failure (e.g. malformed YAML). + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + return new Map(); + } + + throw error; + } }; const workspaceConfigCatalogSchema = z.object({ diff --git a/apps/tasks/src/on-start/index.ts b/apps/tasks/src/on-start/index.ts index cb9564d1b..cc6967dce 100644 --- a/apps/tasks/src/on-start/index.ts +++ b/apps/tasks/src/on-start/index.ts @@ -1,7 +1,7 @@ -import { invalidateUpdateCheckerCacheAsync } from "./invalidate-update-checker-cache"; import { cleanupSessionsAsync } from "./session-cleanup"; +import { warmUpdateCheckerAsync } from "./warm-update-checker"; export async function onStartAsync() { await cleanupSessionsAsync(); - await invalidateUpdateCheckerCacheAsync(); + await warmUpdateCheckerAsync(); } diff --git a/apps/tasks/src/on-start/invalidate-update-checker-cache.ts b/apps/tasks/src/on-start/invalidate-update-checker-cache.ts deleted file mode 100644 index 470989dcf..000000000 --- a/apps/tasks/src/on-start/invalidate-update-checker-cache.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { createLogger } from "@homarr/core/infrastructure/logs"; -import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker"; - -const logger = createLogger({ module: "invalidateUpdateCheckerCache" }); - -/** - * Invalidates the update checker cache on startup to ensure fresh data. - * It is important as we want to avoid showing pending updates after the update to latest version. - */ -export async function invalidateUpdateCheckerCacheAsync() { - try { - const handler = updateCheckerRequestHandler.handler({}); - await handler.invalidateAsync(); - logger.debug("Update checker cache invalidated"); - } catch (error) { - logger.error(new Error("Failed to invalidate update checker cache", { cause: error })); - } -} diff --git a/apps/tasks/src/on-start/warm-update-checker.ts b/apps/tasks/src/on-start/warm-update-checker.ts new file mode 100644 index 000000000..a3b9f94a9 --- /dev/null +++ b/apps/tasks/src/on-start/warm-update-checker.ts @@ -0,0 +1,14 @@ +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker"; + +const logger = createLogger({ module: "warmUpdateChecker" }); + +export async function warmUpdateCheckerAsync() { + try { + const handler = updateCheckerRequestHandler.handler({}); + await handler.getDataAsync(); + logger.debug("Update checker warmed"); + } catch (error) { + logger.error(new Error("Failed to warm update checker", { cause: error })); + } +} diff --git a/deployments/prebuilt-debian/Dockerfile b/deployments/prebuilt-debian/Dockerfile index cfebe0a56..9d6e6af9d 100644 --- a/deployments/prebuilt-debian/Dockerfile +++ b/deployments/prebuilt-debian/Dockerfile @@ -1,4 +1,4 @@ -FROM node:24.17.0-trixie AS base +FROM node:24.18.0-trixie AS base WORKDIR /app COPY package.json . COPY pnpm-lock.yaml . diff --git a/deployments/prebuilt-debian/package.json b/deployments/prebuilt-debian/package.json index 8d59f2567..14a072a77 100644 --- a/deployments/prebuilt-debian/package.json +++ b/deployments/prebuilt-debian/package.json @@ -2,11 +2,11 @@ "name": "homarr-prebuilt-debian", "private": true, "dependencies": { - "better-sqlite3": "^12.10.0" + "better-sqlite3": "^12.11.1" }, "engines": { - "node": ">=24.17.0", - "pnpm": ">=11.8.0" + "node": ">=24.18.0", + "pnpm": ">=11.10.0" }, - "packageManager": "pnpm@11.8.0" + "packageManager": "pnpm@11.10.0" } diff --git a/deployments/prebuilt-debian/pnpm-lock.yaml b/deployments/prebuilt-debian/pnpm-lock.yaml index 0b2a172df..82251eb46 100644 --- a/deployments/prebuilt-debian/pnpm-lock.yaml +++ b/deployments/prebuilt-debian/pnpm-lock.yaml @@ -9,16 +9,16 @@ importers: .: dependencies: better-sqlite3: - specifier: ^12.10.0 - version: 12.10.0 + specifier: ^12.11.1 + version: 12.11.1 packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - better-sqlite3@12.10.0: - resolution: {integrity: sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==} + better-sqlite3@12.11.1: + resolution: {integrity: sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==} engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x} bindings@1.5.0: @@ -148,7 +148,7 @@ snapshots: base64-js@1.5.1: {} - better-sqlite3@12.10.0: + better-sqlite3@12.11.1: dependencies: bindings: 1.5.0 prebuild-install: 7.1.3 diff --git a/docs/README.md b/docs/README.md index 8e37dfdc6..6f281b0f3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -95,18 +95,18 @@ + + Bazarr +
+

Bazarr

+
+ + Beszel

Beszel

- - - - Codeberg -
-

Codeberg

-
@@ -130,13 +130,6 @@ - - Docker Hub -
-

Docker
Hub

-
- - Emby
@@ -144,27 +137,6 @@
- - Github -
-

Github

-
- - - - GitHub Container Registry -
-

GitHub
Container
Registry

-
- - - - Gitlab -
-

Gitlab

-
- - Glances
@@ -172,13 +144,20 @@
+ + Gluetun +
+

Gluetun

+
+ + Gotify

Gotify

- - + + Home Assistant
@@ -205,8 +184,8 @@

Jellyfin

- - + + Jellyseerr
@@ -221,20 +200,13 @@
- - LinuxServer.io -
-

LinuxServer.io

-
- - Navidrome

Navidrome

- - + + Nextcloud
@@ -242,20 +214,13 @@
- - NPM -
-

NPM

-
- - ntfy

ntfy

- - + + NZBGet
@@ -289,6 +254,13 @@

Paperless-ngx

+ + + + PatchMon +
+

PatchMon

+
@@ -303,8 +275,8 @@

Pi-hole

- - + + Plex
@@ -331,15 +303,8 @@

qBittorrent

- - - - Quay -
-

Quay

-
- - + + Radarr
@@ -352,8 +317,8 @@

Readarr

- - + + SABnzbd
@@ -361,13 +326,6 @@
- - Search.ch -
-

Search.ch

-
- - Seerr
@@ -394,6 +352,13 @@

Speedtest
Tracker

+ + + + Synology DiskStation +
+

Synology
DiskStation

+
@@ -401,8 +366,8 @@

Tdarr

- - + + Technitium DNS
@@ -417,6 +382,13 @@
+ + Traefik +
+

Traefik

+
+ + Transmission
@@ -429,8 +401,8 @@

TrueNAS

- - + + Umami
@@ -450,8 +422,8 @@

Unraid

- - + + Uptime Kuma
diff --git a/mise.toml b/mise.toml new file mode 100644 index 000000000..d6bd41fc8 --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[tools] +node = "24.18.0" diff --git a/package.json b/package.json index 040b93355..be1710f32 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homarr", - "version": "1.68.0", + "version": "1.70.0", "private": true, "scripts": { "build": "cross-env CI=true turbo build", @@ -61,10 +61,10 @@ "vitest": "catalog:" }, "engines": { - "node": ">=24.17.0", - "pnpm": ">=11.8.0" + "node": ">=24.18.0", + "pnpm": ">=11.10.0" }, - "packageManager": "pnpm@11.8.0", + "packageManager": "pnpm@11.10.0", "dependencies": { "jsonpath-plus": "catalog:" } diff --git a/packages/analytics/src/send-server-analytics.ts b/packages/analytics/src/send-server-analytics.ts index 2a961d94c..b851f86ec 100644 --- a/packages/analytics/src/send-server-analytics.ts +++ b/packages/analytics/src/send-server-analytics.ts @@ -36,8 +36,6 @@ const logger = createLogger({ module: "analytics" }); type AnalyticsResult = "sent" | "disabled" | "failed"; -// ponytail: no DB-level lock; concurrent calls may both generate an ID, but the last write wins -// and the ID stabilizes after the first successful run. Upgrade path: use DB upsert with WHERE instanceId IS NULL. const getOrCreateInstanceId = async ( analyticsSettings: Awaited>>, ): Promise => { diff --git a/packages/api/src/env.ts b/packages/api/src/env.ts index ba26a264a..a978acf01 100644 --- a/packages/api/src/env.ts +++ b/packages/api/src/env.ts @@ -6,9 +6,11 @@ export const env = createEnv({ server: { KUBERNETES_SERVICE_ACCOUNT_NAME: z.string().optional(), DEMO_MODE: createBooleanSchema(false), + DEMO_READ_ONLY: createBooleanSchema(true), }, runtimeEnv: { KUBERNETES_SERVICE_ACCOUNT_NAME: process.env.KUBERNETES_SERVICE_ACCOUNT_NAME, DEMO_MODE: process.env.DEMO_MODE, + DEMO_READ_ONLY: process.env.DEMO_READ_ONLY, }, }); diff --git a/packages/api/src/mcp.ts b/packages/api/src/mcp.ts index 887e878ad..039943adb 100644 --- a/packages/api/src/mcp.ts +++ b/packages/api/src/mcp.ts @@ -9,6 +9,8 @@ import { iconsRouter } from "./router/icons"; import { infoRouter } from "./router/info"; import { integrationRouter } from "./router/integration/integration-router"; import { inviteRouter } from "./router/invite"; +import { bazarrRouter } from "./router/widgets/bazarr"; +import { serverSettingsRouter } from "./router/serverSettings"; import { beszelRouter } from "./router/widgets/beszel"; import { calendarRouter } from "./router/widgets/calendar"; import { dnsHoleRouter } from "./router/widgets/dns-hole"; @@ -16,7 +18,9 @@ import { downloadsRouter } from "./router/widgets/downloads"; import { healthMonitoringRouter } from "./router/widgets/health-monitoring"; import { mediaRequestsRouter } from "./router/widgets/media-requests"; import { mediaServerRouter } from "./router/widgets/media-server"; +import { patchmonRouter } from "./router/widgets/patchmon"; import { smartHomeRouter } from "./router/widgets/smart-home"; +import { widgetSecretsRouter } from "./router/widgets/widget-secrets"; export const mcpRouter = createTRPCRouter({ app: appRouterForApps, @@ -27,6 +31,8 @@ export const mcpRouter = createTRPCRouter({ info: infoRouter, integration: integrationRouter, invite: inviteRouter, + bazarr: bazarrRouter, + serverSettings: serverSettingsRouter, beszel: beszelRouter, calendar: calendarRouter, dnsHole: dnsHoleRouter, @@ -34,5 +40,7 @@ export const mcpRouter = createTRPCRouter({ healthMonitoring: healthMonitoringRouter, mediaRequests: mediaRequestsRouter, mediaServer: mediaServerRouter, + patchmon: patchmonRouter, smartHome: smartHomeRouter, + widgetSecrets: widgetSecretsRouter, }); diff --git a/packages/api/src/open-api.ts b/packages/api/src/open-api.ts index 5c75cfbc2..05ba467fb 100644 --- a/packages/api/src/open-api.ts +++ b/packages/api/src/open-api.ts @@ -6,6 +6,7 @@ import { appRouter } from "./router/app"; import { boardRouter } from "./router/board"; import { infoRouter } from "./router/info"; import { inviteRouter } from "./router/invite"; +import { serverSettingsRouter } from "./router/serverSettings"; import { userRouter } from "./router/user"; import { createTRPCRouter } from "./trpc"; @@ -14,13 +15,14 @@ export const openApiRouter = createTRPCRouter({ boardRouter, infoRouter, inviteRouter, + serverSettingsRouter, userRouter, }); export const openApiDocument = (base: string) => generateOpenApiDocument(openApiRouter, { title: "Homarr API documentation", - version: "1.0.0", + version: "1.1.0", baseUrl: base, docsUrl: "https://homarr.dev", securitySchemes: { diff --git a/packages/api/src/query-cache.ts b/packages/api/src/query-cache.ts index 690059723..a8cdb9641 100644 --- a/packages/api/src/query-cache.ts +++ b/packages/api/src/query-cache.ts @@ -1,9 +1,11 @@ import type { QueryKey } from "@tanstack/react-query"; -export const queryCacheRetentionMs = 1000 * 60 * 60 * 24; -export const queryCacheDefaultStaleTimeMs = 1000 * 60 * 5; +export const queryCacheDefaultStaleTimeMs = 0; +export const queryCacheDefaultRefetchIntervalMs = 30_000; +export const queryCacheDefaultGcTimeMs = 1000 * 60 * 60 * 24; export const queryCacheMaxValueBytes = 1024 * 1024; export const queryCacheStoragePrefix = "homarr-widget-query"; +export const queryCacheBuster = "v2-superjson"; let activeQueryCacheBoardId: string | null = null; @@ -14,7 +16,7 @@ export const setActiveQueryCacheBoardId = (boardId: string | null) => { export const getActiveQueryCacheBoardId = () => activeQueryCacheBoardId; const persistableQueryPaths = new Set(["app.byId", "app.byIds", "docker.getContainers", "integration.byIds"]); -const excludedWidgetPaths = new Set(["widget.app.ping"]); +const excludedWidgetPaths = new Set(["widget.app.ping", "widget.beszel.getSystemStats"]); const getTrpcPathFromQueryKey = (queryKey: QueryKey) => { const first = queryKey[0]; diff --git a/packages/api/src/router/board.ts b/packages/api/src/router/board.ts index fe8c0dcb9..9cb34d1bd 100644 --- a/packages/api/src/router/board.ts +++ b/packages/api/src/router/board.ts @@ -35,6 +35,7 @@ import { everyoneGroup, getPermissionsWithChildren, getPermissionsWithParents, + widgetDefaultSizes, widgetKinds, } from "@homarr/definitions"; import { importOldmarrAsync } from "@homarr/old-import"; @@ -330,7 +331,16 @@ export const boardRouter = createTRPCRouter({ }), duplicateBoard: permissionRequiredProcedure .requiresPermission("board-create") + .meta({ + openapi: { method: "POST", path: "/api/boards/{id}/duplicate", tags: ["boards"], protect: true }, + mcp: { + enabled: true, + description: + "Duplicate an existing board into a new board. Requires board-create permission and view permission on the source board. REQUIRED: id (source board ID), name (unique name for the new board). Returns { boardId }", + }, + }) .input(boardDuplicateSchema) + .output(z.object({ boardId: z.string() })) .mutation(async ({ ctx, input }) => { await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "view"); await noBoardWithSimilarNameAsync(ctx.db, input.name); @@ -544,33 +554,57 @@ export const boardRouter = createTRPCRouter({ }); }, }); + + return { boardId: newBoardId }; }), - renameBoard: protectedProcedure.input(boardRenameSchema).mutation(async ({ ctx, input }) => { - await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full"); + renameBoard: protectedProcedure + .meta({ + openapi: { method: "PATCH", path: "/api/boards/{id}/name", tags: ["boards"], protect: true }, + mcp: { + enabled: true, + description: + "Rename a board by ID. Requires full permission on the board. REQUIRED: id (board ID), name (new unique board name)", + }, + }) + .input(boardRenameSchema) + .output(z.void()) + .mutation(async ({ ctx, input }) => { + await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full"); - await noBoardWithSimilarNameAsync(ctx.db, input.name, [input.id]); + await noBoardWithSimilarNameAsync(ctx.db, input.name, [input.id]); - await ctx.db.update(boards).set({ name: input.name }).where(eq(boards.id, input.id)); - }), - changeBoardVisibility: protectedProcedure.input(boardChangeVisibilitySchema).mutation(async ({ ctx, input }) => { - await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full"); - const boardSettings = await getServerSettingByKeyAsync(ctx.db, "board"); + await ctx.db.update(boards).set({ name: input.name }).where(eq(boards.id, input.id)); + }), + changeBoardVisibility: protectedProcedure + .meta({ + openapi: { method: "PATCH", path: "/api/boards/{id}/visibility", tags: ["boards"], protect: true }, + mcp: { + enabled: true, + description: + "Change board visibility. Requires full permission on the board. REQUIRED: id (board ID), visibility ('public' or 'private'). Home boards cannot be made private", + }, + }) + .input(boardChangeVisibilitySchema) + .output(z.void()) + .mutation(async ({ ctx, input }) => { + await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full"); + const boardSettings = await getServerSettingByKeyAsync(ctx.db, "board"); - if ( - input.visibility !== "public" && - (boardSettings.homeBoardId === input.id || boardSettings.mobileHomeBoardId === input.id) - ) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Cannot make home board private", - }); - } + if ( + input.visibility !== "public" && + (boardSettings.homeBoardId === input.id || boardSettings.mobileHomeBoardId === input.id) + ) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Cannot make home board private", + }); + } - await ctx.db - .update(boards) - .set({ isPublic: input.visibility === "public" }) - .where(eq(boards.id, input.id)); - }), + await ctx.db + .update(boards) + .set({ isPublic: input.visibility === "public" }) + .where(eq(boards.id, input.id)); + }), deleteBoard: protectedProcedure .meta({ openapi: { method: "DELETE", path: "/api/boards/{id}", tags: ["boards"], protect: true }, @@ -587,16 +621,38 @@ export const boardRouter = createTRPCRouter({ await ctx.db.delete(boards).where(eq(boards.id, input.id)); }), - setHomeBoard: protectedProcedure.input(z.object({ id: z.string() })).mutation(async ({ ctx, input }) => { - await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "view"); + setHomeBoard: protectedProcedure + .meta({ + openapi: { method: "PATCH", path: "/api/boards/{id}/home", tags: ["boards"], protect: true }, + mcp: { + enabled: true, + description: + "Set the current user's desktop home board. Requires view permission on the board. REQUIRED: id (board ID)", + }, + }) + .input(z.object({ id: z.string() })) + .output(z.void()) + .mutation(async ({ ctx, input }) => { + await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "view"); - await ctx.db.update(users).set({ homeBoardId: input.id }).where(eq(users.id, ctx.session.user.id)); - }), - setMobileHomeBoard: protectedProcedure.input(z.object({ id: z.string() })).mutation(async ({ ctx, input }) => { - await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "view"); + await ctx.db.update(users).set({ homeBoardId: input.id }).where(eq(users.id, ctx.session.user.id)); + }), + setMobileHomeBoard: protectedProcedure + .meta({ + openapi: { method: "PATCH", path: "/api/boards/{id}/mobile-home", tags: ["boards"], protect: true }, + mcp: { + enabled: true, + description: + "Set the current user's mobile home board. Requires view permission on the board. REQUIRED: id (board ID)", + }, + }) + .input(z.object({ id: z.string() })) + .output(z.void()) + .mutation(async ({ ctx, input }) => { + await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "view"); - await ctx.db.update(users).set({ mobileHomeBoardId: input.id }).where(eq(users.id, ctx.session.user.id)); - }), + await ctx.db.update(users).set({ mobileHomeBoardId: input.id }).where(eq(users.id, ctx.session.user.id)); + }), getHomeBoard: publicProcedure.query(async ({ ctx }) => { const userId = ctx.session?.user.id; const user = userId @@ -752,7 +808,16 @@ export const boardRouter = createTRPCRouter({ } }), savePartialBoardSettings: protectedProcedure - .input(boardSavePartialSettingsSchema.and(z.object({ id: z.string() }))) + .meta({ + openapi: { method: "PATCH", path: "/api/boards/{id}/settings", tags: ["boards"], protect: true }, + mcp: { + enabled: true, + description: + "Update visual and behavior settings for a board. Requires modify permission. REQUIRED: id (board ID). Optional fields include pageTitle, metaTitle, logoImageUrl, faviconImageUrl, backgroundImageUrl, colors, opacity, customCss, itemRadius, and disableStatus", + }, + }) + .input(boardSavePartialSettingsSchema.extend({ id: z.string() })) + .output(z.void()) .mutation(async ({ ctx, input }) => { await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify"); @@ -1438,21 +1503,33 @@ export const boardRouter = createTRPCRouter({ } } + const defaultSize = widgetDefaultSizes[input.kind as WidgetKind] ?? { width: 1, height: 1 }; + + const fitsAt = (x: number, y: number) => { + if (x + defaultSize.width > layout.columnCount) return false; + for (let dy = 0; dy < defaultSize.height; dy++) { + const row = occupied[y + dy]; + if (!row) continue; + for (let dx = 0; dx < defaultSize.width; dx++) { + if (row[x + dx]) return false; + } + } + return true; + }; + let placed = false; for (let y = 0; y < 9999 && !placed; y++) { if (!occupied[y]) occupied.push(Array.from({ length: layout.columnCount }).fill(false)); - const occupiedRow = occupied[y]; - if (!occupiedRow) continue; for (let x = 0; x < layout.columnCount && !placed; x++) { - if (!occupiedRow[x]) { + if (fitsAt(x, y)) { layoutRows.push({ itemId, sectionId: emptySection.id, layoutId: layout.id, xOffset: x, yOffset: y, - width: 1, - height: 1, + width: defaultSize.width, + height: defaultSize.height, }); placed = true; } diff --git a/packages/api/src/router/docker/docker-router.ts b/packages/api/src/router/docker/docker-router.ts index 0011545d0..44b4ce381 100644 --- a/packages/api/src/router/docker/docker-router.ts +++ b/packages/api/src/router/docker/docker-router.ts @@ -25,9 +25,7 @@ export const dockerRouter = createTRPCRouter({ .concat(dockerMiddleware()) .query(async () => { const innerHandler = dockerContainersRequestHandler.handler({}); - const result = await innerHandler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); + const result = await innerHandler.getDataAsync(); const { data, timestamp } = result; @@ -36,26 +34,6 @@ export const dockerRouter = createTRPCRouter({ timestamp, }; }), - subscribeContainers: permissionRequiredProcedure - .requiresPermission("admin") - .concat(dockerMiddleware()) - .subscription(() => { - return observable((emit) => { - const innerHandler = dockerContainersRequestHandler.handler({}); - const unsubscribe = innerHandler.subscribe((data) => { - emit.next(data); - }); - - return unsubscribe; - }); - }), - invalidate: permissionRequiredProcedure - .requiresPermission("admin") - .concat(dockerMiddleware()) - .mutation(async () => { - const innerHandler = dockerContainersRequestHandler.handler({}); - await innerHandler.invalidateAsync(); - }), startAll: permissionRequiredProcedure .requiresPermission("admin") .meta({ @@ -73,9 +51,6 @@ export const dockerRouter = createTRPCRouter({ await container.start(); }), ); - - const innerHandler = dockerContainersRequestHandler.handler({}); - await innerHandler.invalidateAsync(); }), stopAll: permissionRequiredProcedure .requiresPermission("admin") @@ -94,9 +69,6 @@ export const dockerRouter = createTRPCRouter({ await container.stop(); }), ); - - const innerHandler = dockerContainersRequestHandler.handler({}); - await innerHandler.invalidateAsync(); }), restartAll: permissionRequiredProcedure .requiresPermission("admin") @@ -116,9 +88,6 @@ export const dockerRouter = createTRPCRouter({ await container.restart(); }), ); - - const innerHandler = dockerContainersRequestHandler.handler({}); - await innerHandler.invalidateAsync(); }), removeAll: permissionRequiredProcedure .requiresPermission("admin") @@ -138,9 +107,6 @@ export const dockerRouter = createTRPCRouter({ await container.remove(); }), ); - - const innerHandler = dockerContainersRequestHandler.handler({}); - await innerHandler.invalidateAsync(); }), logs: permissionRequiredProcedure .requiresPermission("admin") diff --git a/packages/api/src/router/icons.ts b/packages/api/src/router/icons.ts index 4f0e59d33..6758dd012 100644 --- a/packages/api/src/router/icons.ts +++ b/packages/api/src/router/icons.ts @@ -30,8 +30,6 @@ export const iconsRouter = createTRPCRouter({ columns: { id: true, name: true, url: true }, where: whereCondition, orderBy: (table, { asc, sql }) => { - // ponytail: SVG first, then alphabetical; with a search term, - // prepend a relevance tier (exact > prefix > substring). const svgFirst = sql`CASE WHEN ${table.name} LIKE '%.svg' THEN 0 ELSE 1 END`; const nameAsc = asc(table.name); if (term.length === 0) { diff --git a/packages/api/src/router/info.ts b/packages/api/src/router/info.ts index a0436ede3..156f6387a 100644 --- a/packages/api/src/router/info.ts +++ b/packages/api/src/router/info.ts @@ -1,7 +1,7 @@ import z from "zod/v4"; import packageJson from "../../../../package.json"; -import { createTRPCRouter, isDemoMode, publicProcedure, protectedProcedure } from "../trpc"; +import { createTRPCRouter, isDemoMode, isDemoReadOnly, publicProcedure, protectedProcedure } from "../trpc"; export const infoRouter = createTRPCRouter({ getInfo: protectedProcedure @@ -20,4 +20,5 @@ export const infoRouter = createTRPCRouter({ }; }), isDemoMode: publicProcedure.query(() => isDemoMode), + isDemoReadOnly: publicProcedure.query(() => isDemoReadOnly), }); diff --git a/packages/api/src/router/query-cache.ts b/packages/api/src/router/query-cache.ts index 6143501fc..25c9f3808 100644 --- a/packages/api/src/router/query-cache.ts +++ b/packages/api/src/router/query-cache.ts @@ -2,53 +2,41 @@ import { z } from "zod/v4"; import { eq } from "@homarr/db"; import { boards } from "@homarr/db/schema"; -import { createQueryCacheChannel } from "@homarr/redis"; +import { removeQueryCacheAsync, setQueryCacheAsync } from "@homarr/redis"; -import { queryCacheMaxValueBytes, queryCacheRetentionMs } from "../query-cache"; -import { createTRPCRouter, publicProcedure } from "../trpc"; +import { queryCacheDefaultGcTimeMs, queryCacheMaxValueBytes } from "../query-cache"; +import { createTRPCRouter, internalProcedure } from "../trpc"; import { throwIfActionForbiddenAsync } from "./board/board-access"; -const queryCacheKeyInput = z.object({ +const queryCacheBoardInput = z.object({ boardId: z.string().min(1), - key: z.string().min(1).max(4096), }); -const queryCacheSetInput = queryCacheKeyInput.extend({ +const queryCacheSetInput = queryCacheBoardInput.extend({ value: z.string().max(queryCacheMaxValueBytes * 2), }); -const createChannel = (input: z.infer, userId: string) => - createQueryCacheChannel({ - userId, - boardId: input.boardId, - key: input.key, - ttlMs: queryCacheRetentionMs, - maxValueBytes: queryCacheMaxValueBytes, - }); - const validateBoardAccessAsync = async (ctx: Parameters[0], boardId: string) => { await throwIfActionForbiddenAsync(ctx, eq(boards.id, boardId), "view"); }; export const queryCacheRouter = createTRPCRouter({ - getItem: publicProcedure.input(queryCacheKeyInput).query(async ({ ctx, input }) => { + setItem: internalProcedure.input(queryCacheSetInput).mutation(async ({ ctx, input }) => { await validateBoardAccessAsync(ctx, input.boardId); - - const channel = createChannel(input, ctx.session?.user.id ?? "anonymous"); - return await channel.getAsync(); - }), - setItem: publicProcedure.input(queryCacheSetInput).mutation(async ({ ctx, input }) => { - await validateBoardAccessAsync(ctx, input.boardId); - - const channel = createChannel(input, ctx.session?.user.id ?? "anonymous"); + const userId = ctx.session?.user.id ?? "anonymous"; return { - stored: await channel.setAsync(input.value), + stored: await setQueryCacheAsync( + userId, + input.boardId, + input.value, + queryCacheDefaultGcTimeMs, + queryCacheMaxValueBytes, + ), }; }), - removeItem: publicProcedure.input(queryCacheKeyInput).mutation(async ({ ctx, input }) => { + removeItem: internalProcedure.input(queryCacheBoardInput).mutation(async ({ ctx, input }) => { await validateBoardAccessAsync(ctx, input.boardId); - - const channel = createChannel(input, ctx.session?.user.id ?? "anonymous"); - await channel.removeAsync(); + const userId = ctx.session?.user.id ?? "anonymous"; + await removeQueryCacheAsync(userId, input.boardId); }), }); diff --git a/packages/api/src/router/serverSettings.ts b/packages/api/src/router/serverSettings.ts index 8ecc870f3..9783e0ae9 100644 --- a/packages/api/src/router/serverSettings.ts +++ b/packages/api/src/router/serverSettings.ts @@ -1,6 +1,14 @@ +import { TRPCError } from "@trpc/server"; import { z } from "zod/v4"; -import { getServerSettingByKeyAsync, getServerSettingsAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries"; +import { and, eq, inArray } from "@homarr/db"; +import { + getServerSettingByKeyAsync, + getServerSettingsAsync, + insertServerSettingByKeyAsync, + updateServerSettingByKeyAsync, +} from "@homarr/db/queries"; +import { boards, serverSettings } from "@homarr/db/schema"; import type { ServerSettings } from "@homarr/server-settings"; import { defaultServerSettingsKeys } from "@homarr/server-settings"; import { settingsInitSchema } from "@homarr/validation/settings"; @@ -8,6 +16,15 @@ import { settingsInitSchema } from "@homarr/validation/settings"; import { createTRPCRouter, onboardingProcedure, permissionRequiredProcedure, publicProcedure } from "../trpc"; import { nextOnboardingStepAsync } from "./onboard/onboard-queries"; +const boardServerSettingsSchema = z.object({ + homeBoardId: z.string().nullable(), + mobileHomeBoardId: z.string().nullable(), + enableStatusByDefault: z.boolean(), + forceDisableStatus: z.boolean(), +}) satisfies z.ZodType; + +const boardServerSettingsUpdateSchema = boardServerSettingsSchema.partial(); + export const serverSettingsRouter = createTRPCRouter({ getCulture: publicProcedure.query(async ({ ctx }) => { return await getServerSettingByKeyAsync(ctx.db, "culture"); @@ -15,6 +32,67 @@ export const serverSettingsRouter = createTRPCRouter({ getAll: permissionRequiredProcedure.requiresPermission("admin").query(async ({ ctx }) => { return await getServerSettingsAsync(ctx.db); }), + getBoardSettings: permissionRequiredProcedure + .requiresPermission("admin") + .meta({ + openapi: { method: "GET", path: "/api/settings/board", tags: ["settings"], protect: true }, + mcp: { + enabled: true, + description: + "Get global board defaults, including desktop/mobile home board IDs and status behavior. Requires admin permission", + }, + }) + .input(z.void()) + .output(boardServerSettingsSchema) + .query(async ({ ctx }) => { + return await getServerSettingByKeyAsync(ctx.db, "board"); + }), + updateBoardSettings: permissionRequiredProcedure + .requiresPermission("admin") + .meta({ + openapi: { method: "PATCH", path: "/api/settings/board", tags: ["settings"], protect: true }, + mcp: { + enabled: true, + description: + "Update global board defaults. Requires admin permission. Optional fields: homeBoardId, mobileHomeBoardId, enableStatusByDefault, forceDisableStatus. Home board IDs must reference public boards or be null", + }, + }) + .input(boardServerSettingsUpdateSchema) + .output(boardServerSettingsSchema) + .mutation(async ({ ctx, input }) => { + const inputBoardIds = [input.homeBoardId, input.mobileHomeBoardId].filter( + (id) => id !== undefined && id !== null, + ); + + if (inputBoardIds.length > 0) { + const publicBoards = await ctx.db.query.boards.findMany({ + columns: { id: true }, + where: and(inArray(boards.id, inputBoardIds), eq(boards.isPublic, true)), + }); + const publicBoardIds = new Set(publicBoards.map((board) => board.id)); + const invalidBoardIds = inputBoardIds.filter((id) => !publicBoardIds.has(id)); + if (invalidBoardIds.length > 0) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `Board settings home board IDs must reference public boards: ${invalidBoardIds.join(", ")}`, + }); + } + } + + const current = await getServerSettingByKeyAsync(ctx.db, "board"); + const next = { ...current, ...input }; + const existing = await ctx.db.query.serverSettings.findFirst({ + where: eq(serverSettings.settingKey, "board"), + }); + + if (existing) { + await updateServerSettingByKeyAsync(ctx.db, "board", next); + } else { + await insertServerSettingByKeyAsync(ctx.db, "board", next); + } + + return next; + }), saveSettings: permissionRequiredProcedure .requiresPermission("admin") .input( diff --git a/packages/api/src/router/test/docker/docker-router.spec.ts b/packages/api/src/router/test/docker/docker-router.spec.ts index cbea4063b..4a5a32a88 100644 --- a/packages/api/src/router/test/docker/docker-router.spec.ts +++ b/packages/api/src/router/test/docker/docker-router.spec.ts @@ -23,28 +23,12 @@ vi.mock("@homarr/request-handler/docker", () => ({ }, dockerContainersRequestHandler: { handler: () => ({ - getCachedOrUpdatedDataAsync: async () => { - return await Promise.resolve({ containers: [] }); - }, - invalidateAsync: async () => { - return await Promise.resolve(); + getDataAsync: async () => { + return await Promise.resolve({ data: [], timestamp: new Date() }); }, }), }, })); -vi.mock("@homarr/redis", () => ({ - createCacheChannel: () => ({ - // eslint-disable-next-line @typescript-eslint/require-await - consumeAsync: async () => ({ - timestamp: new Date().toISOString(), - data: { containers: [] }, - }), - // eslint-disable-next-line @typescript-eslint/no-empty-function - invalidateAsync: async () => {}, - }), - createWidgetOptionsChannel: () => ({}), -})); - vi.mock("@homarr/docker/env", () => ({ env: { ENABLE_DOCKER: true, @@ -67,12 +51,10 @@ const validInputs: { [key in (typeof procedureKeys)[number]]: RouterInputs["docker"][key]; } = { getContainers: undefined, - subscribeContainers: undefined, startAll: { ids: ["1"] }, stopAll: { ids: ["1"] }, restartAll: { ids: ["1"] }, removeAll: { ids: ["1"] }, - invalidate: undefined, logs: { id: "1", tail: 200 }, subscribeLogs: { id: "1", tail: 200 }, }; diff --git a/packages/api/src/router/test/integration/integration-test-connection.spec.ts b/packages/api/src/router/test/integration/integration-test-connection.spec.ts index f2519c533..0da8356c4 100644 --- a/packages/api/src/router/test/integration/integration-test-connection.spec.ts +++ b/packages/api/src/router/test/integration/integration-test-connection.spec.ts @@ -270,78 +270,4 @@ describe("testConnectionAsync should run test connection of integration", () => externalUrl: null, }); }); - - test("with input of existing github app", async () => { - // Arrange - const factorySpy = vi.spyOn(homarrIntegrations, "createIntegrationAsync"); - const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions"); - factorySpy.mockReturnValue( - Promise.resolve({ - testConnectionAsync: async () => await Promise.resolve({ success: true }), - } as homarrIntegrations.PiHoleIntegrationV6), - ); - optionsSpy.mockReturnValue([[], ["githubAppId", "githubInstallationId", "privateKey"]]); - - const integration = { - id: "new", - name: "GitHub", - url: "https://api.github.com", - kind: "github" as const, - secrets: [ - { - kind: "githubAppId" as const, - value: "345", - }, - { - kind: "githubInstallationId" as const, - value: "456", - }, - { - kind: "privateKey" as const, - value: null, - }, - ], - }; - - const dbSecrets = [ - { - kind: "githubAppId" as const, - value: "123.encrypted" as const, - }, - { - kind: "githubInstallationId" as const, - value: "234.encrypted" as const, - }, - { - kind: "privateKey" as const, - value: "privateKey.encrypted" as const, - }, - ]; - - // Act - await testConnectionAsync(integration, dbSecrets); - - // Assert - expect(factorySpy).toHaveBeenCalledWith({ - id: "new", - name: "GitHub", - url: "https://api.github.com", - kind: "github" as const, - decryptedSecrets: [ - expect.objectContaining({ - kind: "githubAppId", - value: "345", - }), - expect.objectContaining({ - kind: "githubInstallationId", - value: "456", - }), - expect.objectContaining({ - kind: "privateKey", - value: "privateKey", - }), - ], - externalUrl: null, - }); - }); }); diff --git a/packages/api/src/router/test/serverSettings.spec.ts b/packages/api/src/router/test/serverSettings.spec.ts index 443d82b6f..c045b1288 100644 --- a/packages/api/src/router/test/serverSettings.spec.ts +++ b/packages/api/src/router/test/serverSettings.spec.ts @@ -1,9 +1,9 @@ -import SuperJSON from "superjson"; +import { stringify } from "superjson"; import { describe, expect, test, vi } from "vitest"; import type { Session } from "@homarr/auth"; import { createId } from "@homarr/common"; -import { serverSettings } from "@homarr/db/schema"; +import { boards, serverSettings } from "@homarr/db/schema"; import { createDb } from "@homarr/db/test"; import { defaultServerSettings, defaultServerSettingsKeys } from "@homarr/server-settings"; @@ -33,7 +33,7 @@ describe("getAll server settings", () => { await db.insert(serverSettings).values([ { settingKey: defaultServerSettingsKeys[0], - value: SuperJSON.stringify(defaultServerSettings.analytics), + value: stringify(defaultServerSettings.analytics), }, ]); @@ -67,7 +67,7 @@ describe("saveSettings", () => { await db.insert(serverSettings).values([ { settingKey: defaultServerSettingsKeys[0], - value: SuperJSON.stringify(defaultServerSettings.analytics), + value: stringify(defaultServerSettings.analytics), }, ]); @@ -82,7 +82,7 @@ describe("saveSettings", () => { expect(dbSettings).toStrictEqual([ { settingKey: "analytics", - value: SuperJSON.stringify({ + value: stringify({ enableGeneral: true, instanceId: null, }), @@ -90,3 +90,91 @@ describe("saveSettings", () => { ]); }); }); + +describe("board settings API", () => { + test("getBoardSettings should return board defaults", async () => { + const db = createDb(); + const caller = serverSettingsRouter.createCaller({ + db, + deviceType: undefined, + session: defaultSession, + }); + + await expect(caller.getBoardSettings()).resolves.toStrictEqual(defaultServerSettings.board); + }); + + test("updateBoardSettings should insert settings when defaults are not persisted", async () => { + const db = createDb(); + const caller = serverSettingsRouter.createCaller({ + db, + deviceType: undefined, + session: defaultSession, + }); + const boardId = createId(); + await db.insert(boards).values({ id: boardId, name: "default", isPublic: true }); + + const result = await caller.updateBoardSettings({ + homeBoardId: boardId, + forceDisableStatus: true, + }); + + expect(result).toStrictEqual({ + ...defaultServerSettings.board, + homeBoardId: boardId, + forceDisableStatus: true, + }); + const dbSettings = await db.select().from(serverSettings); + expect(dbSettings).toStrictEqual([ + { + settingKey: "board", + value: stringify(result), + }, + ]); + }); + + test("updateBoardSettings should update existing settings", async () => { + const db = createDb(); + const caller = serverSettingsRouter.createCaller({ + db, + deviceType: undefined, + session: defaultSession, + }); + const boardId = createId(); + await db.insert(boards).values({ id: boardId, name: "default", isPublic: true }); + + await caller.updateBoardSettings({ homeBoardId: boardId }); + const result = await caller.updateBoardSettings({ + mobileHomeBoardId: boardId, + enableStatusByDefault: false, + }); + + expect(result).toStrictEqual({ + ...defaultServerSettings.board, + homeBoardId: boardId, + mobileHomeBoardId: boardId, + enableStatusByDefault: false, + }); + const dbSettings = await db.select().from(serverSettings); + expect(dbSettings).toStrictEqual([ + { + settingKey: "board", + value: stringify(result), + }, + ]); + }); + + test("updateBoardSettings should reject private home boards", async () => { + const db = createDb(); + const caller = serverSettingsRouter.createCaller({ + db, + deviceType: undefined, + session: defaultSession, + }); + const boardId = createId(); + await db.insert(boards).values({ id: boardId, name: "private", isPublic: false }); + + const actAsync = async () => await caller.updateBoardSettings({ homeBoardId: boardId }); + + await expect(actAsync()).rejects.toThrow("must reference public boards"); + }); +}); diff --git a/packages/api/src/router/test/widgets/patchmon.spec.ts b/packages/api/src/router/test/widgets/patchmon.spec.ts new file mode 100644 index 000000000..df37b5647 --- /dev/null +++ b/packages/api/src/router/test/widgets/patchmon.spec.ts @@ -0,0 +1,200 @@ +import { TRPCError } from "@trpc/server"; +import { describe, expect, test, vi } from "vitest"; + +import type { Session } from "@homarr/auth"; +import { createId } from "@homarr/common"; +import type { Database } from "@homarr/db"; +import { boards, integrationItems, integrations, integrationUserPermissions, items, users } from "@homarr/db/schema"; +import { createDb } from "@homarr/db/test"; +import type { PatchMonStats } from "@homarr/integrations"; + +import { patchmonRouter } from "../../widgets/patchmon"; + +const samplePatchMonStats = { + totalHosts: 42, + hostsNeedingUpdates: 15, + securityUpdates: 23, + upToDateHosts: 27, + hostsWithSecurityUpdates: 8, + recentUpdates24h: 34, + totalOutdatedPackages: 156, + totalRepos: 12, + lastUpdated: "2025-10-11T12:34:56.789Z", + osDistribution: [], +} satisfies PatchMonStats; + +vi.mock("@homarr/request-handler/patchmon", () => ({ + patchmonStatsRequestHandler: { + handler: () => ({ + getDataAsync: async () => ({ + data: samplePatchMonStats, + timestamp: new Date(), + }), + }), + }, +})); + +const createSession = (userId: string): Session => ({ + user: { + id: userId, + permissions: [], + colorScheme: "light", + }, + expires: new Date().toISOString(), +}); + +const createCaller = (db: Database, session: Session | null) => + patchmonRouter.createCaller({ + db, + deviceType: undefined, + session, + }); + +const createPatchMonIntegrationOnBoardAsync = async ( + db: Database, + options: { + isPublic: boolean; + boardCreatorId: string; + }, +) => { + const integrationId = createId(); + const boardId = createId(); + const itemId = createId(); + + await db.insert(integrations).values({ + id: integrationId, + kind: "patchmon", + name: "PatchMon", + url: "https://patchmon.example.com", + }); + + await db.insert(boards).values({ + id: boardId, + name: "patchmon-board", + creatorId: options.boardCreatorId, + isPublic: options.isPublic, + }); + + await db.insert(items).values({ + id: itemId, + kind: "patchmon", + boardId, + options: "{}", + }); + + await db.insert(integrationItems).values({ + integrationId, + itemId, + }); + + return { integrationId, boardId, itemId }; +}; + +describe("patchmonRouter.getStats access control", () => { + test("should throw UNAUTHORIZED for unauthenticated users even when integration is on a public board", async () => { + const db = createDb(); + const otherUserId = createId(); + await db.insert(users).values({ id: otherUserId }); + const { integrationId } = await createPatchMonIntegrationOnBoardAsync(db, { + isPublic: true, + boardCreatorId: otherUserId, + }); + const caller = createCaller(db, null); + + await expect(caller.getStats({ integrationId })).rejects.toThrow( + new TRPCError({ code: "UNAUTHORIZED" }), + ); + }); + + test("should throw FORBIDDEN for authenticated users without board or integration access", async () => { + const db = createDb(); + const boardOwnerId = createId(); + const sessionUserId = createId(); + await db.insert(users).values([{ id: boardOwnerId }, { id: sessionUserId }]); + const { integrationId } = await createPatchMonIntegrationOnBoardAsync(db, { + isPublic: false, + boardCreatorId: boardOwnerId, + }); + const caller = createCaller(db, createSession(sessionUserId)); + + await expect(caller.getStats({ integrationId })).rejects.toThrow( + new TRPCError({ + code: "FORBIDDEN", + message: "User does not have permission to query at least one of the specified integration", + }), + ); + }); + + test("should return stats for authenticated users when integration is on a public board", async () => { + const db = createDb(); + const boardOwnerId = createId(); + const sessionUserId = createId(); + await db.insert(users).values([{ id: boardOwnerId }, { id: sessionUserId }]); + const { integrationId } = await createPatchMonIntegrationOnBoardAsync(db, { + isPublic: true, + boardCreatorId: boardOwnerId, + }); + const caller = createCaller(db, createSession(sessionUserId)); + + const result = await caller.getStats({ integrationId }); + + expect(result).toStrictEqual(samplePatchMonStats); + }); + + test("should return stats for authenticated users with integration use permission and no board placement", async () => { + const db = createDb(); + const sessionUserId = createId(); + const integrationId = createId(); + await db.insert(users).values({ id: sessionUserId }); + await db.insert(integrations).values({ + id: integrationId, + kind: "patchmon", + name: "PatchMon", + url: "https://patchmon.example.com", + }); + await db.insert(integrationUserPermissions).values({ + integrationId, + userId: sessionUserId, + permission: "use", + }); + const caller = createCaller(db, createSession(sessionUserId)); + + const result = await caller.getStats({ integrationId }); + + expect(result).toStrictEqual(samplePatchMonStats); + }); + + test("should throw NOT_FOUND when integration id does not exist", async () => { + const db = createDb(); + const sessionUserId = createId(); + await db.insert(users).values({ id: sessionUserId }); + const caller = createCaller(db, createSession(sessionUserId)); + + await expect(caller.getStats({ integrationId: createId() })).rejects.toMatchObject({ + code: "NOT_FOUND", + }); + }); + + test("should throw NOT_FOUND when integration is not a PatchMon integration", async () => { + const db = createDb(); + const sessionUserId = createId(); + const integrationId = createId(); + await db.insert(users).values({ id: sessionUserId }); + await db.insert(integrations).values({ + id: integrationId, + kind: "sonarr", + name: "Sonarr", + url: "https://sonarr.example.com", + }); + await db.insert(integrationUserPermissions).values({ + integrationId, + userId: sessionUserId, + permission: "use", + }); + const caller = createCaller(db, createSession(sessionUserId)); + + await expect(caller.getStats({ integrationId })).rejects.toMatchObject({ + code: "NOT_FOUND", + }); + }); +}); diff --git a/packages/api/src/router/update-checker.ts b/packages/api/src/router/update-checker.ts index 8957be5f2..8cb6c097e 100644 --- a/packages/api/src/router/update-checker.ts +++ b/packages/api/src/router/update-checker.ts @@ -9,7 +9,7 @@ export const updateCheckerRouter = createTRPCRouter({ getAvailableUpdates: permissionRequiredProcedure.requiresPermission("admin").query(async () => { try { const handler = updateCheckerRequestHandler.handler({}); - const data = await handler.getCachedOrUpdatedDataAsync({}); + const data = await handler.getDataAsync(); return data.data.availableUpdates; } catch (error) { logger.error(new Error("Failed to get available updates", { cause: error })); diff --git a/packages/api/src/router/widgets/anchor-notes.ts b/packages/api/src/router/widgets/anchor-notes.ts index 7e5b7043c..d431c0de9 100644 --- a/packages/api/src/router/widgets/anchor-notes.ts +++ b/packages/api/src/router/widgets/anchor-notes.ts @@ -45,9 +45,7 @@ export const anchorNotesRouter = createTRPCRouter({ .query(async ({ ctx, input }) => { const handler = anchorNotesListRequestHandler.handler(ctx.integration, { ...input, limit: input.limit ?? 50 }); - const { data } = await handler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); + const { data } = await handler.getDataAsync(); return data; }), @@ -57,9 +55,7 @@ export const anchorNotesRouter = createTRPCRouter({ .query(async ({ ctx, input }) => { const handler = anchorNoteRequestHandler.handler(ctx.integration, { noteId: input.noteId }); - const { data } = await handler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); + const { data } = await handler.getDataAsync(); return data; }), @@ -79,9 +75,6 @@ export const anchorNotesRouter = createTRPCRouter({ ...(normalizedContent !== undefined ? { content: normalizedContent } : {}), }); - const noteHandler = anchorNoteRequestHandler.handler(ctx.integration, { noteId: input.noteId }); - await noteHandler.invalidateAsync(); - return updatedNote; } catch (error) { if (error instanceof ResponseError && error.statusCode === 400) { diff --git a/packages/api/src/router/widgets/app.ts b/packages/api/src/router/widgets/app.ts index d07241725..92e1298da 100644 --- a/packages/api/src/router/widgets/app.ts +++ b/packages/api/src/router/widgets/app.ts @@ -1,10 +1,8 @@ import { TRPCError } from "@trpc/server"; -import { observable } from "@trpc/server/observable"; import { z } from "zod/v4"; import { getServerSettingByKeyAsync } from "@homarr/db/queries"; import { sendPingRequestAsync } from "@homarr/ping"; -import { pingChannel, pingUrlChannel } from "@homarr/redis"; import { createTRPCRouter, publicProcedure } from "../../trpc"; import { AppRepository } from "../app"; @@ -45,55 +43,4 @@ export const appRouter = createTRPCRouter({ ...pingResult, }; }), - updatedPing: publicProcedure - .input( - z.object({ - id: z.string(), - }), - ) - .subscription(async ({ ctx, input }) => { - const boardSettings = await getServerSettingByKeyAsync(ctx.db, "board"); - if (boardSettings.forceDisableStatus) { - throw new TRPCError({ - code: "FORBIDDEN", - message: "Ping functionality is disabled by server settings", - }); - } - - const repository = new AppRepository(ctx.db, ctx.session?.user ?? null); - const app = await repository.getByIdAsync(input.id); - - if (!app) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "App not found", - }); - } - - const pingUrl = app.pingUrl ?? app.href; - - if (!pingUrl) { - throw new TRPCError({ - code: "CONFLICT", - message: "No URL to ping configured for this app", - }); - } - - await pingUrlChannel.addAsync(pingUrl); - - return observable<{ url: string; statusCode: number; durationMs: number } | { url: string; error: string }>( - (emit) => { - const unsubscribe = pingChannel.subscribe((message) => { - // Only emit if same url - if (message.url !== pingUrl) return; - emit.next(message); - }); - - return () => { - unsubscribe(); - void pingUrlChannel.removeAsync(pingUrl); - }; - }, - ); - }), }); diff --git a/packages/api/src/router/widgets/archive-team-warrior.ts b/packages/api/src/router/widgets/archive-team-warrior.ts index 0824c165a..d71b7546a 100644 --- a/packages/api/src/router/widgets/archive-team-warrior.ts +++ b/packages/api/src/router/widgets/archive-team-warrior.ts @@ -1,6 +1,3 @@ -import { observable } from "@trpc/server/observable"; - -import type { ArchiveTeamWarriorStatus } from "@homarr/integrations"; import { archiveTeamWarriorRequestHandler } from "@homarr/request-handler/archive-team-warrior"; import { createOneIntegrationMiddleware } from "../../middlewares/integration"; @@ -11,29 +8,11 @@ export const archiveTeamWarriorRouter = createTRPCRouter({ .concat(createOneIntegrationMiddleware("query", "archiveTeamWarrior")) .query(async ({ ctx }) => { const handler = archiveTeamWarriorRequestHandler.handler(ctx.integration, {}); - const { data, timestamp } = await handler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data, timestamp } = await handler.getDataAsync(); return { status: data, updatedAt: timestamp, }; }), - - subscribeStatus: publicProcedure - .concat(createOneIntegrationMiddleware("query", "archiveTeamWarrior")) - .subscription(({ ctx }) => { - return observable<{ status: ArchiveTeamWarriorStatus; updatedAt: Date }>((emit) => { - const handler = archiveTeamWarriorRequestHandler.handler(ctx.integration, {}); - const unsubscribe = handler.subscribe((status) => { - emit.next({ - status, - updatedAt: new Date(), - }); - }); - - return () => { - unsubscribe(); - }; - }); - }), }); diff --git a/packages/api/src/router/widgets/audio-stats.ts b/packages/api/src/router/widgets/audio-stats.ts index d43876583..14fd7d49c 100644 --- a/packages/api/src/router/widgets/audio-stats.ts +++ b/packages/api/src/router/widgets/audio-stats.ts @@ -13,14 +13,14 @@ export const audioStatsRouter = createTRPCRouter({ const fetchByKind = { navidrome: async () => { const handler = navidromeRequestHandler.handler({ ...ctx.integration, kind: "navidrome" as const }, {}); - return (await handler.getCachedOrUpdatedDataAsync({ forceUpdate: false })).data; + return (await handler.getDataAsync()).data; }, audiobookshelf: async () => { const handler = audiobookshelfRequestHandler.handler( { ...ctx.integration, kind: "audiobookshelf" as const }, {}, ); - return (await handler.getCachedOrUpdatedDataAsync({ forceUpdate: false })).data; + return (await handler.getDataAsync()).data; }, } as const; diff --git a/packages/api/src/router/widgets/bazarr.ts b/packages/api/src/router/widgets/bazarr.ts new file mode 100644 index 000000000..c47ad4173 --- /dev/null +++ b/packages/api/src/router/widgets/bazarr.ts @@ -0,0 +1,21 @@ +import { bazarrBadgesRequestHandler } from "@homarr/request-handler/bazarr"; + +import { createOneIntegrationMiddleware } from "../../middlewares/integration"; +import { createTRPCRouter, publicProcedure } from "../../trpc"; + +export const bazarrRouter = createTRPCRouter({ + getBadges: publicProcedure + .meta({ + mcp: { + enabled: true, + description: + "Get missing subtitle counts, provider issues, and health warnings for a Bazarr integration. REQUIRED: integrationId from integration_all", + }, + }) + .concat(createOneIntegrationMiddleware("query", "bazarr")) + .query(async ({ ctx }) => { + const innerHandler = bazarrBadgesRequestHandler.handler(ctx.integration, {}); + const data = await innerHandler.getDataAsync(); + return data.data; + }), +}); diff --git a/packages/api/src/router/widgets/beszel.ts b/packages/api/src/router/widgets/beszel.ts index cbb453d03..81fae10a0 100644 --- a/packages/api/src/router/widgets/beszel.ts +++ b/packages/api/src/router/widgets/beszel.ts @@ -4,19 +4,21 @@ import { z } from "zod/v4"; import { createLogger } from "@homarr/core/infrastructure/logs"; import { createIntegrationAsync } from "@homarr/integrations"; -import type { BeszelContainerStatsRecord, BeszelSystemStatsRecord } from "@homarr/integrations/types"; +import type { LiveStatsEvent } from "@homarr/integrations/types"; import { beszelAlertsRequestHandler, beszelStatsRequestHandler, beszelSystemsRequestHandler, } from "@homarr/request-handler/beszel"; -import type { BeszelAlertsData, BeszelSystemRow } from "@homarr/request-handler/beszel"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; import { createTRPCRouter, publicProcedure } from "../../trpc"; const logger = createLogger({ module: "beszelRouter" }); +const errorMessage = (error: unknown) => (error instanceof Error ? error.message : String(error)); + export const beszelRouter = createTRPCRouter({ getSystems: publicProcedure .meta({ @@ -30,12 +32,11 @@ export const beszelRouter = createTRPCRouter({ .query(async ({ ctx }) => { const integrationIds = ctx.integrations.map((i) => i.id); logger.debug("getSystems called", { userId: ctx.session?.user?.id, integrationIds }); - const settled = await Promise.allSettled( - ctx.integrations.map(async (integration) => { + const results = await settleIntegrationQueries( + ctx.integrations, + async (integration) => { const innerHandler = beszelSystemsRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); + const { data, timestamp } = await innerHandler.getDataAsync(); return { integrationId: integration.id, integrationName: integration.name, @@ -43,25 +44,18 @@ export const beszelRouter = createTRPCRouter({ systems: data, updatedAt: timestamp, }; - }), + }, + { + fallback: (integration, error) => ({ + integrationId: integration.id, + integrationName: integration.name, + integrationUrl: integration.url, + systems: [], + updatedAt: new Date(0), + error: errorMessage(error), + }), + }, ); - const results = settled.map((result, index) => { - if (result.status === "fulfilled") return result.value; - const integration = ctx.integrations[index]; - logger.warn("getSystems integration failed", { - userId: ctx.session?.user?.id, - integrationId: integration?.id, - error: result.reason, - }); - return { - integrationId: integration?.id ?? "unknown", - integrationName: integration?.name ?? "unknown", - integrationUrl: integration?.url ?? "", - systems: [], - updatedAt: new Date(0), - error: result.reason instanceof Error ? result.reason.message : String(result.reason), - }; - }); logger.debug("getSystems completed", { userId: ctx.session?.user?.id, integrationIds, @@ -71,30 +65,6 @@ export const beszelRouter = createTRPCRouter({ return results; }), - subscribeSystems: publicProcedure - .concat(createManyIntegrationMiddleware("query", "beszel", "mock")) - .subscription(({ ctx }) => { - return observable<{ - integrationId: string; - systems: BeszelSystemRow[]; - timestamp: Date; - }>((emit) => { - const unsubscribes = ctx.integrations.map((integration) => { - const innerHandler = beszelSystemsRequestHandler.handler(integration, {}); - return innerHandler.subscribe((systems) => { - emit.next({ - integrationId: integration.id, - systems, - timestamp: new Date(), - }); - }); - }); - return () => { - unsubscribes.forEach((fn) => fn()); - }; - }); - }), - getAlerts: publicProcedure .meta({ mcp: { @@ -118,20 +88,25 @@ export const beszelRouter = createTRPCRouter({ includeHistory: input.includeHistory, maxHistoryItems: input.maxHistoryItems, }); - const settled = await Promise.allSettled( - ctx.integrations.map(async (integration) => { + const results = await settleIntegrationQueries( + ctx.integrations, + async (integration) => { const alertsHandler = beszelAlertsRequestHandler.handler(integration, { includeHistory: input.includeHistory, maxHistoryItems: input.maxHistoryItems, }); const systemsHandler = beszelSystemsRequestHandler.handler(integration, {}); - const [alertsResult, systemsResult] = await Promise.all([ - alertsHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }), - systemsHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }), + const [alertsSettled, systemsSettled] = await Promise.allSettled([ + alertsHandler.getDataAsync(), + systemsHandler.getDataAsync(), ]); + if (alertsSettled.status === "rejected") throw alertsSettled.reason; + const alertsResult = alertsSettled.value; const systemNameMap: Record = {}; - for (const system of systemsResult.data) { - systemNameMap[system.id] = system.name; + if (systemsSettled.status === "fulfilled") { + for (const system of systemsSettled.value.data) { + systemNameMap[system.id] = system.name; + } } return { integrationId: integration.id, @@ -141,26 +116,19 @@ export const beszelRouter = createTRPCRouter({ systemNameMap, updatedAt: alertsResult.timestamp, }; - }), + }, + { + fallback: (integration, error) => ({ + integrationId: integration.id, + integrationName: integration.name, + alerts: [], + history: [], + systemNameMap: {}, + updatedAt: new Date(0), + error: errorMessage(error), + }), + }, ); - const results = settled.map((result, index) => { - if (result.status === "fulfilled") return result.value; - const integration = ctx.integrations[index]; - logger.warn("getAlerts integration failed", { - userId: ctx.session?.user?.id, - integrationId: integration?.id, - error: result.reason, - }); - return { - integrationId: integration?.id ?? "unknown", - integrationName: integration?.name ?? "unknown", - alerts: [], - history: [], - systemNameMap: {}, - updatedAt: new Date(0), - error: result.reason instanceof Error ? result.reason.message : String(result.reason), - }; - }); logger.debug("getAlerts completed", { userId: ctx.session?.user?.id, integrationIds, @@ -170,39 +138,6 @@ export const beszelRouter = createTRPCRouter({ return results; }), - subscribeAlerts: publicProcedure - .concat(createManyIntegrationMiddleware("query", "beszel", "mock")) - .input( - z.object({ - includeHistory: z.boolean().default(true), - maxHistoryItems: z.number().min(1).max(100).default(10), - }), - ) - .subscription(({ ctx, input }) => { - return observable<{ - integrationId: string; - alerts: BeszelAlertsData; - timestamp: Date; - }>((emit) => { - const unsubscribes = ctx.integrations.map((integration) => { - const innerHandler = beszelAlertsRequestHandler.handler(integration, { - includeHistory: input.includeHistory, - maxHistoryItems: input.maxHistoryItems, - }); - return innerHandler.subscribe((data) => { - emit.next({ - integrationId: integration.id, - alerts: data, - timestamp: new Date(), - }); - }); - }); - return () => { - unsubscribes.forEach((fn) => fn()); - }; - }); - }), - getSystemStats: publicProcedure .meta({ mcp: { @@ -237,7 +172,7 @@ export const beszelRouter = createTRPCRouter({ timePeriod: input.timePeriod, includeDocker: input.includeDocker, }); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data, timestamp } = await innerHandler.getDataAsync(); logger.debug("getSystemStats completed", { userId: ctx.session?.user?.id, integrationId: integration.id, @@ -256,7 +191,7 @@ export const beszelRouter = createTRPCRouter({ systemStats: [], containerStats: [], updatedAt: new Date(0), - error: error instanceof Error ? error.message : String(error), + error: errorMessage(error), }; } }), @@ -269,51 +204,96 @@ export const beszelRouter = createTRPCRouter({ }), ) .subscription(({ ctx, input }) => { - return observable<{ - stats: BeszelSystemStatsRecord; - containerStats: BeszelContainerStatsRecord | null; - }>((emit) => { - const integration = ctx.integrations[0]; - if (!integration) { - emit.error(new Error("No Beszel integration configured")); - return () => {}; - } - logger.debug("subscribeSystemStats opened", { + return observable((emit) => { + const controller = new AbortController(); + let isActive = true; + let emittedEventCount = 0; + + logger.debug("Beszel realtime subscription started", { userId: ctx.session?.user?.id, - integrationId: integration.id, + integrationIds: ctx.integrations.map((integration) => integration.id), systemId: input.systemId, }); - const abortController = new AbortController(); - void (async () => { + const integration = ctx.integrations[0]; + if (!integration) { + emit.error( + new TRPCError({ code: "BAD_REQUEST", message: "At least one Beszel integrationId is required" }), + ); + return; + } + try { const instance = await createIntegrationAsync(integration); - await instance.subscribeRealtimeMetrics(input.systemId, (data) => emit.next(data), abortController.signal); + if (typeof instance.subscribeRealtimeMetrics === "function") { + await instance.subscribeRealtimeMetrics( + input.systemId, + (event) => { + if (!isActive) return; + emittedEventCount += 1; + if (emittedEventCount <= 2 || emittedEventCount % 60 === 0) { + logger.debug("Forwarding Beszel realtime events", { + userId: ctx.session?.user?.id, + integrationId: integration.id, + systemId: input.systemId, + eventType: event.type, + emittedEventCount, + statsCount: Array.isArray(event.record.stats) ? event.record.stats.length : undefined, + }); + } + emit.next(event); + }, + controller.signal, + ); + } else { + emit.error( + new TRPCError({ + code: "METHOD_NOT_SUPPORTED", + message: `Integration ${integration.kind} does not support realtime metrics`, + }), + ); + } } catch (error) { - if (!abortController.signal.aborted) { - logger.warn("subscribeSystemStats error", { + if (isActive) { + logger.warn("Beszel realtime subscription failed", { userId: ctx.session?.user?.id, integrationId: integration.id, systemId: input.systemId, - error, + emittedEventCount, + error: errorMessage(error), }); - if (error instanceof Error) { - emit.error(error); - } else { - emit.error(new Error(String(error))); - } + emit.error( + error instanceof TRPCError + ? error + : new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: error instanceof Error ? error.message : String(error), + }), + ); } } + + if (isActive) { + logger.debug("Beszel realtime subscription completed", { + userId: ctx.session?.user?.id, + integrationId: integration.id, + systemId: input.systemId, + emittedEventCount, + }); + emit.complete(); + } })(); return () => { - abortController.abort(); - logger.debug("subscribeSystemStats closed", { + isActive = false; + logger.debug("Beszel realtime subscription cancelled", { userId: ctx.session?.user?.id, - integrationId: integration.id, + integrationIds: ctx.integrations.map((integration) => integration.id), systemId: input.systemId, + emittedEventCount, }); + controller.abort(); }; }); }), diff --git a/packages/api/src/router/widgets/calendar.ts b/packages/api/src/router/widgets/calendar.ts index 50ba71b3e..729e82634 100644 --- a/packages/api/src/router/widgets/calendar.ts +++ b/packages/api/src/router/widgets/calendar.ts @@ -1,13 +1,8 @@ -import { observable } from "@trpc/server/observable"; import { z } from "zod/v4"; -import type { Modify } from "@homarr/common/types"; import { createLogger } from "@homarr/core/infrastructure/logs"; import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; -import type { Integration } from "@homarr/db/schema"; -import type { IntegrationKindByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; -import type { CalendarEvent } from "@homarr/integrations/types"; import { radarrReleaseTypes } from "@homarr/integrations/types"; import { calendarMonthRequestHandler } from "@homarr/request-handler/calendar"; @@ -39,9 +34,7 @@ export const calendarRouter = createTRPCRouter({ ctx.integrations.map(async (integration) => { const { integrationIds: _integrationIds, ...handlerInput } = input; const innerHandler = calendarMonthRequestHandler.handler(integration, handlerInput); - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); + const { data } = await innerHandler.getDataAsync(); return { events: data, @@ -72,39 +65,4 @@ export const calendarRouter = createTRPCRouter({ return []; }); }), - subscribeToEvents: publicProcedure - .input( - z.object({ - year: z.number(), - month: z.number(), - releaseType: z.array(z.enum(radarrReleaseTypes)), - showUnmonitored: z.boolean(), - }), - ) - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("calendar"))) - .subscription(({ ctx, input }) => { - return observable<{ - integration: Modify }>; - events: CalendarEvent[]; - }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integrationWithSecrets of ctx.integrations) { - const { decryptedSecrets: _, ...integration } = integrationWithSecrets; - const { integrationIds: _integrationIds, ...handlerInput } = input; - const innerHandler = calendarMonthRequestHandler.handler(integrationWithSecrets, handlerInput); - const unsubscribe = innerHandler.subscribe((events) => { - emit.next({ - integration, - events, - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); - }; - }); - }), }); diff --git a/packages/api/src/router/widgets/coolify.ts b/packages/api/src/router/widgets/coolify.ts index 8bdaa6f3e..9476039d9 100644 --- a/packages/api/src/router/widgets/coolify.ts +++ b/packages/api/src/router/widgets/coolify.ts @@ -1,49 +1,23 @@ -import { observable } from "@trpc/server/observable"; - -import type { CoolifyInstanceInfo } from "@homarr/integrations/types"; import { coolifyRequestHandler } from "@homarr/request-handler/coolify"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; export const coolifyRouter = createTRPCRouter({ getInstancesInfo: publicProcedure .concat(createManyIntegrationMiddleware("query", "coolify")) .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = coolifyRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const innerHandler = coolifyRequestHandler.handler(integration, {}); + const { data, timestamp } = await innerHandler.getDataAsync(); - return { - integrationId: integration.id, - integrationName: integration.name, - integrationUrl: integration.url, - instanceInfo: data, - updatedAt: timestamp, - }; - }), - ); - - return results; - }), - subscribeInstancesInfo: publicProcedure - .concat(createManyIntegrationMiddleware("query", "coolify")) - .subscription(({ ctx }) => { - return observable<{ integrationId: string; instanceInfo: CoolifyInstanceInfo; timestamp: Date }>((emit) => { - const unsubscribes = ctx.integrations.map((integration) => { - const innerHandler = coolifyRequestHandler.handler(integration, {}); - return innerHandler.subscribe((instanceInfo) => { - emit.next({ - integrationId: integration.id, - instanceInfo, - timestamp: new Date(), - }); - }); - }); - - return () => { - unsubscribes.forEach((unsubscribe) => unsubscribe()); + return { + integrationId: integration.id, + integrationName: integration.name, + integrationUrl: integration.url, + instanceInfo: data, + updatedAt: timestamp, }; }); }), diff --git a/packages/api/src/router/widgets/dns-hole.ts b/packages/api/src/router/widgets/dns-hole.ts index 93162a1e3..32f15da98 100644 --- a/packages/api/src/router/widgets/dns-hole.ts +++ b/packages/api/src/router/widgets/dns-hole.ts @@ -1,15 +1,11 @@ -import { observable } from "@trpc/server/observable"; import { z } from "zod/v4"; -import type { Modify } from "@homarr/common/types"; -import type { Integration } from "@homarr/db/schema"; -import type { IntegrationKindByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; -import type { DnsHoleSummary } from "@homarr/integrations/types"; import { dnsHoleRequestHandler } from "@homarr/request-handler/dns-hole"; import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc"; export const dnsHoleRouter = createTRPCRouter({ @@ -23,50 +19,11 @@ export const dnsHoleRouter = createTRPCRouter({ }) .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("dnsHole"))) .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = dnsHoleRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); - - return { - integration: { - id: integration.id, - name: integration.name, - kind: integration.kind, - updatedAt: timestamp, - }, - summary: data, - }; - }), - ); - return results; - }), - - subscribeToSummary: publicProcedure - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("dnsHole"))) - .subscription(({ ctx }) => { - return observable<{ - integration: Modify }>; - summary: DnsHoleSummary; - }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integrationWithSecrets of ctx.integrations) { - const { decryptedSecrets: _, ...integration } = integrationWithSecrets; - const innerHandler = dnsHoleRequestHandler.handler(integrationWithSecrets, {}); - const unsubscribe = innerHandler.subscribe((summary) => { - emit.next({ - integration, - summary, - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const { data, timestamp } = await dnsHoleRequestHandler.handler(integration, {}).getDataAsync(); + return { + integration: { id: integration.id, name: integration.name, kind: integration.kind, updatedAt: timestamp }, + summary: data, }; }); }), @@ -83,12 +40,6 @@ export const dnsHoleRouter = createTRPCRouter({ .mutation(async ({ ctx: { integration } }) => { const client = await createIntegrationAsync(integration); await client.enableAsync(); - - const innerHandler = dnsHoleRequestHandler.handler(integration, {}); - // We need to wait for the integration to be enabled before invalidating the cache - await new Promise((resolve) => { - setTimeout(() => void innerHandler.invalidateAsync().then(resolve), 1000); - }); }), disable: protectedProcedure @@ -108,11 +59,5 @@ export const dnsHoleRouter = createTRPCRouter({ .mutation(async ({ ctx: { integration }, input }) => { const client = await createIntegrationAsync(integration); await client.disableAsync(input.duration); - - const innerHandler = dnsHoleRequestHandler.handler(integration, {}); - // We need to wait for the integration to be disabled before invalidating the cache - await new Promise((resolve) => { - setTimeout(() => void innerHandler.invalidateAsync().then(resolve), 1000); - }); }), }); diff --git a/packages/api/src/router/widgets/downloads.ts b/packages/api/src/router/widgets/downloads.ts index 682decdc4..bdce2473f 100644 --- a/packages/api/src/router/widgets/downloads.ts +++ b/packages/api/src/router/widgets/downloads.ts @@ -1,16 +1,12 @@ -import { observable } from "@trpc/server/observable"; import { z } from "zod/v4"; -import type { Modify } from "@homarr/common/types"; -import type { Integration } from "@homarr/db/schema"; -import type { IntegrationKindByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; -import type { DownloadClientJobsAndStatus } from "@homarr/integrations"; import { createIntegrationAsync, downloadClientItemSchema } from "@homarr/integrations"; import { downloadClientRequestHandler } from "@homarr/request-handler/downloads"; import type { IntegrationAction } from "../../middlewares/integration"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc"; const createDownloadClientIntegrationMiddleware = (action: IntegrationAction) => @@ -28,52 +24,12 @@ export const downloadsRouter = createTRPCRouter({ .concat(createDownloadClientIntegrationMiddleware("query")) .input(z.object({ limitPerIntegration: z.number().default(50) })) .query(async ({ ctx, input }) => { - return await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = downloadClientRequestHandler.handler(integration, { limit: input.limitPerIntegration }); - - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); - - return { - integration: { - id: integration.id, - name: integration.name, - kind: integration.kind, - updatedAt: timestamp, - }, - data, - }; - }), - ); - }), - subscribeToJobsAndStatuses: publicProcedure - .concat(createDownloadClientIntegrationMiddleware("query")) - .input(z.object({ limitPerIntegration: z.number().default(50) })) - .subscription(({ ctx, input }) => { - return observable<{ - integration: Modify }>; - data: DownloadClientJobsAndStatus; - }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integrationWithSecrets of ctx.integrations) { - const { decryptedSecrets: _, ...integration } = integrationWithSecrets; - const innerHandler = downloadClientRequestHandler.handler(integrationWithSecrets, { - limit: input.limitPerIntegration, - }); - const unsubscribe = innerHandler.subscribe((data) => { - emit.next({ - integration, - data, - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const innerHandler = downloadClientRequestHandler.handler(integration, { limit: input.limitPerIntegration }); + const { data, timestamp } = await innerHandler.getDataAsync(); + return { + integration: { id: integration.id, name: integration.name, kind: integration.kind, updatedAt: timestamp }, + data, }; }); }), @@ -93,6 +49,7 @@ export const downloadsRouter = createTRPCRouter({ await integrationInstance.pauseQueueAsync(); }), ); + downloadClientRequestHandler.invalidateCache(); }), pauseItem: protectedProcedure .concat(createDownloadClientIntegrationMiddleware("interact")) @@ -104,6 +61,7 @@ export const downloadsRouter = createTRPCRouter({ await integrationInstance.pauseItemAsync(input.item); }), ); + downloadClientRequestHandler.invalidateCache(); }), resume: protectedProcedure .meta({ @@ -121,6 +79,7 @@ export const downloadsRouter = createTRPCRouter({ await integrationInstance.resumeQueueAsync(); }), ); + downloadClientRequestHandler.invalidateCache(); }), resumeItem: protectedProcedure .concat(createDownloadClientIntegrationMiddleware("interact")) @@ -132,6 +91,7 @@ export const downloadsRouter = createTRPCRouter({ await integrationInstance.resumeItemAsync(input.item); }), ); + downloadClientRequestHandler.invalidateCache(); }), deleteItem: protectedProcedure .concat(createDownloadClientIntegrationMiddleware("interact")) @@ -143,5 +103,6 @@ export const downloadsRouter = createTRPCRouter({ await integrationInstance.deleteItemAsync(input.item, input.fromDisk); }), ); + downloadClientRequestHandler.invalidateCache(); }), }); diff --git a/packages/api/src/router/widgets/firewall.ts b/packages/api/src/router/widgets/firewall.ts index 2d582cb1f..08951da7c 100644 --- a/packages/api/src/router/widgets/firewall.ts +++ b/packages/api/src/router/widgets/firewall.ts @@ -1,15 +1,4 @@ -import { observable } from "@trpc/server/observable"; - -import type { Modify } from "@homarr/common/types"; -import type { Integration } from "@homarr/db/schema"; -import type { IntegrationKindByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; -import type { - FirewallCpuSummary, - FirewallInterfacesSummary, - FirewallMemorySummary, - FirewallVersionSummary, -} from "@homarr/integrations"; import { firewallCpuRequestHandler, firewallInterfacesRequestHandler, @@ -18,198 +7,31 @@ import { } from "@homarr/request-handler/firewall"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; +const firewallMiddleware = createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall")); + +const queryFirewall = < + THandler extends { + handler: (integration: any, input: any) => { getDataAsync: () => Promise<{ data: any; timestamp: Date }> }; + }, +>( + handler: THandler, +) => + publicProcedure.concat(firewallMiddleware).query(async ({ ctx }) => + settleIntegrationQueries(ctx.integrations, async (integration) => { + const { data, timestamp } = await handler.handler(integration, {}).getDataAsync(); + return { + integration: { id: integration.id, name: integration.name, kind: integration.kind, updatedAt: timestamp }, + summary: data as Awaited["getDataAsync"]>>["data"], + }; + }), + ); + export const firewallRouter = createTRPCRouter({ - getFirewallCpuStatus: publicProcedure - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"))) - .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = firewallCpuRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); - - return { - integration: { - id: integration.id, - name: integration.name, - kind: integration.kind, - updatedAt: timestamp, - }, - summary: data, - }; - }), - ); - return results; - }), - subscribeFirewallCpuStatus: publicProcedure - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"))) - .subscription(({ ctx }) => { - return observable<{ - integration: Modify }>; - summary: FirewallCpuSummary; - }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integrationWithSecrets of ctx.integrations) { - const { decryptedSecrets: _, ...integration } = integrationWithSecrets; - const innerHandler = firewallCpuRequestHandler.handler(integrationWithSecrets, {}); - const unsubscribe = innerHandler.subscribe((summary) => { - emit.next({ - integration, - summary, - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); - }; - }); - }), - - getFirewallInterfacesStatus: publicProcedure - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"))) - .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = firewallInterfacesRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); - - return { - integration: { - id: integration.id, - name: integration.name, - kind: integration.kind, - updatedAt: timestamp, - }, - summary: data, - }; - }), - ); - return results; - }), - subscribeFirewallInterfacesStatus: publicProcedure - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"))) - .subscription(({ ctx }) => { - return observable<{ - integration: Modify }>; - summary: FirewallInterfacesSummary[]; - }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integrationWithSecrets of ctx.integrations) { - const { decryptedSecrets: _, ...integration } = integrationWithSecrets; - const innerHandler = firewallInterfacesRequestHandler.handler(integrationWithSecrets, {}); - const unsubscribe = innerHandler.subscribe((summary) => { - emit.next({ - integration, - summary, - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); - }; - }); - }), - - getFirewallVersionStatus: publicProcedure - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"))) - .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = firewallVersionRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); - - return { - integration: { - id: integration.id, - name: integration.name, - kind: integration.kind, - updatedAt: timestamp, - }, - summary: data, - }; - }), - ); - return results; - }), - subscribeFirewallVersionStatus: publicProcedure - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"))) - .subscription(({ ctx }) => { - return observable<{ - integration: Modify }>; - summary: FirewallVersionSummary; - }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integrationWithSecrets of ctx.integrations) { - const { decryptedSecrets: _, ...integration } = integrationWithSecrets; - const innerHandler = firewallVersionRequestHandler.handler(integrationWithSecrets, {}); - const unsubscribe = innerHandler.subscribe((summary) => { - emit.next({ - integration, - summary, - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); - }; - }); - }), - - getFirewallMemoryStatus: publicProcedure - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"))) - .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = firewallMemoryRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); - - return { - integration: { - id: integration.id, - name: integration.name, - kind: integration.kind, - updatedAt: timestamp, - }, - summary: data, - }; - }), - ); - return results; - }), - subscribeFirewallMemoryStatus: publicProcedure - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"))) - .subscription(({ ctx }) => { - return observable<{ - integration: Modify }>; - summary: FirewallMemorySummary; - }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integrationWithSecrets of ctx.integrations) { - const { decryptedSecrets: _, ...integration } = integrationWithSecrets; - const innerHandler = firewallMemoryRequestHandler.handler(integrationWithSecrets, {}); - const unsubscribe = innerHandler.subscribe((summary) => { - emit.next({ - integration, - summary, - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); - }; - }); - }), + getFirewallCpuStatus: queryFirewall(firewallCpuRequestHandler), + getFirewallInterfacesStatus: queryFirewall(firewallInterfacesRequestHandler), + getFirewallVersionStatus: queryFirewall(firewallVersionRequestHandler), + getFirewallMemoryStatus: queryFirewall(firewallMemoryRequestHandler), }); diff --git a/packages/api/src/router/widgets/health-monitoring.ts b/packages/api/src/router/widgets/health-monitoring.ts index 03856f5a0..1957f459c 100644 --- a/packages/api/src/router/widgets/health-monitoring.ts +++ b/packages/api/src/router/widgets/health-monitoring.ts @@ -1,70 +1,59 @@ -import { observable } from "@trpc/server/observable"; - -import type { SystemHealthMonitoring } from "@homarr/integrations"; -import type { ProxmoxClusterInfo } from "@homarr/integrations/types"; +import { createIntegrationAsync } from "@homarr/integrations"; +import { SynologyIntegration } from "@homarr/integrations"; import { clusterInfoRequestHandler, systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring"; import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; +const healthMonitoringIntegrationKinds = [ + "openmediavault", + "dashDot", + "truenas", + "unraid", + "glances", + "synology", + "mock", +] as const; + export const healthMonitoringRouter = createTRPCRouter({ getSystemHealthStatus: publicProcedure .meta({ mcp: { enabled: true, description: - "Get system health status (CPU, memory, disk, network) from NAS/server monitoring integrations. REQUIRED: integrationIds (array of TrueNAS/Unraid/Glances/OpenMediaVault/DashDot integration IDs from integration_all)", + "Get system health status (CPU, memory, disk, network) from NAS/server monitoring integrations. REQUIRED: integrationIds (array of TrueNAS/Synology/Unraid/Glances/OpenMediaVault/DashDot integration IDs from integration_all)", }, }) - .concat( - createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "truenas", "unraid", "glances", "mock"), - ) + .concat(createManyIntegrationMiddleware("query", ...healthMonitoringIntegrationKinds)) .query(async ({ ctx }) => { - return await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = systemInfoRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); - - return { - integrationId: integration.id, - integrationName: integration.name, - healthInfo: data, - updatedAt: timestamp, - }; - }), - ); - }), - subscribeSystemHealthStatus: publicProcedure - .concat( - createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "truenas", "unraid", "glances", "mock"), - ) - .subscription(({ ctx }) => { - return observable<{ - integrationId: string; - healthInfo: SystemHealthMonitoring; - timestamp: Date; - }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integration of ctx.integrations) { - const innerHandler = systemInfoRequestHandler.handler(integration, {}); - const unsubscribe = innerHandler.subscribe((healthInfo) => { - emit.next({ - integrationId: integration.id, - healthInfo, - timestamp: new Date(), - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const { data, timestamp } = await systemInfoRequestHandler.handler(integration, {}).getDataAsync(); + return { + integrationId: integration.id, + integrationName: integration.name, + healthInfo: data, + updatedAt: timestamp, }; }); }), + listStorageVolumes: publicProcedure + .meta({ + mcp: { + enabled: true, + description: + "List storage volumes from a Synology DiskStation integration for widget configuration. REQUIRED: integrationId from integration_all (Synology integration only)", + }, + }) + .concat(createOneIntegrationMiddleware("query", "synology")) + .query(async ({ ctx }) => { + const integrationInstance = await createIntegrationAsync(ctx.integration); + if (!(integrationInstance instanceof SynologyIntegration)) { + throw new Error("Expected Synology integration"); + } + + return await integrationInstance.listStorageVolumesAsync(); + }), getClusterHealthStatus: publicProcedure .meta({ mcp: { @@ -76,24 +65,7 @@ export const healthMonitoringRouter = createTRPCRouter({ .concat(createOneIntegrationMiddleware("query", "proxmox", "mock")) .query(async ({ ctx }) => { const innerHandler = clusterInfoRequestHandler.handler(ctx.integration, {}); - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); + const { data } = await innerHandler.getDataAsync(); return data; }), - subscribeClusterHealthStatus: publicProcedure - .concat(createOneIntegrationMiddleware("query", "proxmox", "mock")) - .subscription(({ ctx }) => { - return observable((emit) => { - const unsubscribes: (() => void)[] = []; - const innerHandler = clusterInfoRequestHandler.handler(ctx.integration, {}); - const unsubscribe = innerHandler.subscribe((healthInfo) => { - emit.next(healthInfo); - }); - unsubscribes.push(unsubscribe); - return () => { - unsubscribe(); - }; - }); - }), }); diff --git a/packages/api/src/router/widgets/immich.ts b/packages/api/src/router/widgets/immich.ts index f1428a80c..7e8c54d49 100644 --- a/packages/api/src/router/widgets/immich.ts +++ b/packages/api/src/router/widgets/immich.ts @@ -17,7 +17,7 @@ const createImmichIntegrationMiddleware = (action: IntegrationAction) => export const immichRouter = createTRPCRouter({ getServerStats: publicProcedure.concat(createImmichIntegrationMiddleware("query")).query(async ({ ctx }) => { const innerHandler = immichStatsRequestHandler.handler(ctx.integration, {}); - const data = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const data = await innerHandler.getDataAsync(); return data.data; }), @@ -30,13 +30,13 @@ export const immichRouter = createTRPCRouter({ ) .query(async ({ ctx, input }) => { const innerHandler = immichAlbumRequestHandler.handler(ctx.integration, { albumId: input.albumId }); - const data = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const data = await innerHandler.getDataAsync(); return data.data; }), getAlbums: publicProcedure.concat(createImmichIntegrationMiddleware("query")).query(async ({ ctx }) => { const innerHandler = immichAlbumsRequestHandler.handler(ctx.integration, {}); - const data = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const data = await innerHandler.getDataAsync(); return data.data; }), }); diff --git a/packages/api/src/router/widgets/index.ts b/packages/api/src/router/widgets/index.ts index 6fea031fa..f99910817 100644 --- a/packages/api/src/router/widgets/index.ts +++ b/packages/api/src/router/widgets/index.ts @@ -9,6 +9,8 @@ export const widgetRouter = createTRPCRouter({ coolify: lazy(() => import("./coolify").then((mod) => mod.coolifyRouter)), immich: lazy(() => import("./immich").then((mod) => mod.immichRouter)), paperlessNgx: lazy(() => import("./paperless-ngx").then((mod) => mod.paperlessNgxRouter)), + patchmon: lazy(() => import("./patchmon").then((mod) => mod.patchmonRouter)), + bazarr: lazy(() => import("./bazarr").then((mod) => mod.bazarrRouter)), notebook: lazy(() => import("./notebook").then((mod) => mod.notebookRouter)), weather: lazy(() => import("./weather").then((mod) => mod.weatherRouter)), app: lazy(() => import("./app").then((mod) => mod.appRouter)), @@ -20,6 +22,7 @@ export const widgetRouter = createTRPCRouter({ calendar: lazy(() => import("./calendar").then((mod) => mod.calendarRouter)), downloads: lazy(() => import("./downloads").then((mod) => mod.downloadsRouter)), mediaRequests: lazy(() => import("./media-requests").then((mod) => mod.mediaRequestsRouter)), + mediaOrganizer: lazy(() => import("./media-organizer").then((mod) => mod.mediaOrganizerRouter)), rssFeed: lazy(() => import("./rssFeed").then((mod) => mod.rssFeedRouter)), indexerManager: lazy(() => import("./indexer-manager").then((mod) => mod.indexerManagerRouter)), healthMonitoring: lazy(() => import("./health-monitoring").then((mod) => mod.healthMonitoringRouter)), @@ -38,6 +41,8 @@ export const widgetRouter = createTRPCRouter({ umami: lazy(() => import("./umami").then((mod) => mod.umamiRouter)), vpn: lazy(() => import("./vpn").then((mod) => mod.vpnRouter)), ups: lazy(() => import("./ups").then((mod) => mod.upsRouter)), + traefik: lazy(() => import("./traefik").then((mod) => mod.traefikRouter)), customApi: lazy(() => import("./custom-api").then((mod) => mod.customApiRouter)), openWebUi: lazy(() => import("./open-webui").then((mod) => mod.openWebUiRouter)), + secrets: lazy(() => import("./widget-secrets").then((mod) => mod.widgetSecretsRouter)), }); diff --git a/packages/api/src/router/widgets/indexer-manager.ts b/packages/api/src/router/widgets/indexer-manager.ts index cb8b88262..54a949dab 100644 --- a/packages/api/src/router/widgets/indexer-manager.ts +++ b/packages/api/src/router/widgets/indexer-manager.ts @@ -1,13 +1,12 @@ import { TRPCError } from "@trpc/server"; -import { observable } from "@trpc/server/observable"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; -import type { Indexer } from "@homarr/integrations/types"; import { indexerManagerRequestHandler } from "@homarr/request-handler/indexer-manager"; import type { IntegrationAction } from "../../middlewares/integration"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc"; const createIndexerManagerIntegrationMiddleware = (action: IntegrationAction) => @@ -17,39 +16,13 @@ export const indexerManagerRouter = createTRPCRouter({ getIndexersStatus: publicProcedure .concat(createIndexerManagerIntegrationMiddleware("query")) .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = indexerManagerRequestHandler.handler(integration, {}); - const { data: indexers } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const innerHandler = indexerManagerRequestHandler.handler(integration, {}); + const { data: indexers } = await innerHandler.getDataAsync(); - return { - integrationId: integration.id, - indexers, - }; - }), - ); - return results; - }), - - subscribeIndexersStatus: publicProcedure - .concat(createIndexerManagerIntegrationMiddleware("query")) - .subscription(({ ctx }) => { - return observable<{ integrationId: string; indexers: Indexer[] }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integrationWithSecrets of ctx.integrations) { - const innerHandler = indexerManagerRequestHandler.handler(integrationWithSecrets, {}); - const unsubscribe = innerHandler.subscribe((indexers) => { - emit.next({ - integrationId: integrationWithSecrets.id, - indexers, - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); + return { + integrationId: integration.id, + indexers, }; }); }), diff --git a/packages/api/src/router/widgets/media-organizer.ts b/packages/api/src/router/widgets/media-organizer.ts new file mode 100644 index 000000000..dd0d687cb --- /dev/null +++ b/packages/api/src/router/widgets/media-organizer.ts @@ -0,0 +1,33 @@ +import { z } from "zod/v4"; + +import { getIntegrationKindsByCategory } from "@homarr/definitions"; +import { mediaOrganizerRequestHandler } from "@homarr/request-handler/media-organizer"; + +import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { createTRPCRouter, publicProcedure } from "../../trpc"; + +export const mediaOrganizerRouter = createTRPCRouter({ + getData: publicProcedure + .meta({ + mcp: { + enabled: true, + description: + "Get missing and queued movies/episodes from Radarr and Sonarr. Requires query (use) access to each integration. REQUIRED: integrationIds (array of Radarr/Sonarr integration IDs from integration_all). OPTIONAL: pageSize (1-50, default 10)", + }, + }) + .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaOrganizer"))) + .input(z.object({ pageSize: z.number().min(1).max(50).default(10) })) + .query(async ({ ctx, input }) => { + const results = await Promise.all( + ctx.integrations.map(async (integration) => { + const innerHandler = mediaOrganizerRequestHandler.handler(integration, { pageSize: input.pageSize }); + const { data } = await innerHandler.getDataAsync(); + return { + integrationId: integration.id, + ...data, + }; + }), + ); + return results; + }), +}); diff --git a/packages/api/src/router/widgets/media-release.ts b/packages/api/src/router/widgets/media-release.ts index fddc174cc..f05a46142 100644 --- a/packages/api/src/router/widgets/media-release.ts +++ b/packages/api/src/router/widgets/media-release.ts @@ -1,35 +1,28 @@ -import { observable } from "@trpc/server/observable"; - -import type { Modify } from "@homarr/common/types"; -import type { Integration } from "@homarr/db/schema"; -import type { IntegrationKindByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; -import type { MediaRelease } from "@homarr/integrations/types"; import { mediaReleaseRequestHandler } from "@homarr/request-handler/media-release"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; export const mediaReleaseRouter = createTRPCRouter({ getMediaReleases: publicProcedure .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRelease"))) .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = mediaReleaseRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const results = await settleIntegrationQueries(ctx.integrations, async (integration) => { + const innerHandler = mediaReleaseRequestHandler.handler(integration, {}); + const { data, timestamp } = await innerHandler.getDataAsync(); - return { - integration: { - id: integration.id, - name: integration.name, - kind: integration.kind, - updatedAt: timestamp, - }, - releases: data, - }; - }), - ); + return { + integration: { + id: integration.id, + name: integration.name, + kind: integration.kind, + updatedAt: timestamp, + }, + releases: data, + }; + }); return results.flatMap((result) => result.releases.map((release) => ({ ...release, @@ -37,31 +30,4 @@ export const mediaReleaseRouter = createTRPCRouter({ })), ); }), - - subscribeToReleases: publicProcedure - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRelease"))) - .subscription(({ ctx }) => { - return observable<{ - integration: Modify }>; - releases: MediaRelease[]; - }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integrationWithSecrets of ctx.integrations) { - const { decryptedSecrets: _, ...integration } = integrationWithSecrets; - const innerHandler = mediaReleaseRequestHandler.handler(integrationWithSecrets, {}); - const unsubscribe = innerHandler.subscribe((releases) => { - emit.next({ - integration, - releases, - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); - }; - }); - }), }); diff --git a/packages/api/src/router/widgets/media-requests.ts b/packages/api/src/router/widgets/media-requests.ts index 715ae1327..99c4906e3 100644 --- a/packages/api/src/router/widgets/media-requests.ts +++ b/packages/api/src/router/widgets/media-requests.ts @@ -1,14 +1,13 @@ -import { observable } from "@trpc/server/observable"; import { z } from "zod/v4"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import { mediaRequestStatusConfiguration } from "@homarr/integrations/types"; -import type { MediaRequest } from "@homarr/integrations/types"; import { mediaRequestListRequestHandler } from "@homarr/request-handler/media-request-list"; import { mediaRequestStatsRequestHandler } from "@homarr/request-handler/media-request-stats"; import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc"; export const mediaRequestsRouter = createTRPCRouter({ @@ -22,22 +21,10 @@ export const mediaRequestsRouter = createTRPCRouter({ }) .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRequest"))) .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = mediaRequestListRequestHandler.handler(integration, {}); - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); - return { - integration: { - id: integration.id, - name: integration.name, - kind: integration.kind, - }, - data, - }; - }), - ); + const results = await settleIntegrationQueries(ctx.integrations, async (integration) => { + const { data } = await mediaRequestListRequestHandler.handler(integration, {}).getDataAsync(); + return { integration: { id: integration.id, name: integration.name, kind: integration.kind }, data }; + }); return results .flatMap(({ data, integration }) => data.map((request) => ({ @@ -56,32 +43,6 @@ export const mediaRequestsRouter = createTRPCRouter({ ); }); }), - subscribeToLatestRequests: publicProcedure - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRequest"))) - .subscription(({ ctx }) => { - return observable<{ - integrationId: string; - requests: MediaRequest[]; - }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integrationWithSecrets of ctx.integrations) { - const { decryptedSecrets: _, ...integration } = integrationWithSecrets; - const innerHandler = mediaRequestListRequestHandler.handler(integrationWithSecrets, {}); - const unsubscribe = innerHandler.subscribe((requests) => { - emit.next({ - integrationId: integration.id, - requests, - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); - }; - }); - }), getStats: publicProcedure .meta({ mcp: { @@ -92,22 +53,10 @@ export const mediaRequestsRouter = createTRPCRouter({ }) .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRequest"))) .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = mediaRequestStatsRequestHandler.handler(integration, {}); - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); - return { - integration: { - id: integration.id, - name: integration.name, - kind: integration.kind, - }, - data, - }; - }), - ); + const results = await settleIntegrationQueries(ctx.integrations, async (integration) => { + const { data } = await mediaRequestStatsRequestHandler.handler(integration, {}).getDataAsync(); + return { integration: { id: integration.id, name: integration.name, kind: integration.kind }, data }; + }); return { stats: results.flatMap((result) => result.data.stats), users: results @@ -139,7 +88,6 @@ export const mediaRequestsRouter = createTRPCRouter({ ) .mutation(async ({ ctx: { integration }, input }) => { const integrationInstance = await createIntegrationAsync(integration); - const innerHandler = mediaRequestListRequestHandler.handler(integration, {}); const answerActions = { approve: (id: number) => integrationInstance.approveRequestAsync(id), @@ -147,6 +95,5 @@ export const mediaRequestsRouter = createTRPCRouter({ } as const; await answerActions[input.answer](input.requestId); - await innerHandler.invalidateAsync(); }), }); diff --git a/packages/api/src/router/widgets/media-server.ts b/packages/api/src/router/widgets/media-server.ts index de61fef75..3e3e64a4f 100644 --- a/packages/api/src/router/widgets/media-server.ts +++ b/packages/api/src/router/widgets/media-server.ts @@ -1,12 +1,11 @@ -import { observable } from "@trpc/server/observable"; import { z } from "zod/v4"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; -import type { StreamSession } from "@homarr/integrations"; import { mediaServerRequestHandler } from "@homarr/request-handler/media-server"; import type { IntegrationAction } from "../../middlewares/integration"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; const createMediaServerIntegrationMiddleware = (action: IntegrationAction) => @@ -24,46 +23,11 @@ export const mediaServerRouter = createTRPCRouter({ .concat(createMediaServerIntegrationMiddleware("query")) .input(z.object({ showOnlyPlaying: z.boolean() })) .query(async ({ ctx, input }) => { - return await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = mediaServerRequestHandler.handler(integration, { - showOnlyPlaying: input.showOnlyPlaying, - }); - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); - return { - integrationId: integration.id, - integrationKind: integration.kind, - sessions: data, - }; - }), - ); - }), - subscribeToCurrentStreams: publicProcedure - .concat(createMediaServerIntegrationMiddleware("query")) - .input(z.object({ showOnlyPlaying: z.boolean() })) - .subscription(({ ctx, input }) => { - return observable<{ integrationId: string; data: StreamSession[] }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integration of ctx.integrations) { - const innerHandler = mediaServerRequestHandler.handler(integration, { - showOnlyPlaying: input.showOnlyPlaying, - }); - - const unsubscribe = innerHandler.subscribe((sessions) => { - emit.next({ - integrationId: integration.id, - data: sessions, - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); - }; + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const { data } = await mediaServerRequestHandler + .handler(integration, { showOnlyPlaying: input.showOnlyPlaying }) + .getDataAsync(); + return { integrationId: integration.id, integrationKind: integration.kind, sessions: data }; }); }), }); diff --git a/packages/api/src/router/widgets/media-transcoding.ts b/packages/api/src/router/widgets/media-transcoding.ts index b02f0a714..88bd91704 100644 --- a/packages/api/src/router/widgets/media-transcoding.ts +++ b/packages/api/src/router/widgets/media-transcoding.ts @@ -1,7 +1,4 @@ -import { observable } from "@trpc/server/observable"; - import { getIntegrationKindsByCategory } from "@homarr/definitions"; -import type { MediaTranscoding } from "@homarr/request-handler/media-transcoding"; import { mediaTranscodingRequestHandler } from "@homarr/request-handler/media-transcoding"; import { paginatedSchema } from "@homarr/validation/common"; @@ -21,26 +18,11 @@ export const mediaTranscodingRouter = createTRPCRouter({ pageOffset: (input.page - 1) * input.pageSize, pageSize: input.pageSize, }); - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data } = await innerHandler.getDataAsync(); return { integrationId: ctx.integration.id, data, }; }), - subscribeData: publicProcedure - .concat(createIndexerManagerIntegrationMiddleware("query")) - .input(paginatedSchema.pick({ page: true, pageSize: true })) - .subscription(({ ctx, input }) => { - return observable<{ integrationId: string; data: MediaTranscoding }>((emit) => { - const innerHandler = mediaTranscodingRequestHandler.handler(ctx.integration, { - pageOffset: (input.page - 1) * input.pageSize, - pageSize: input.pageSize, - }); - const unsubscribe = innerHandler.subscribe((data) => { - emit.next({ integrationId: input.integrationId, data }); - }); - return unsubscribe; - }); - }), }); diff --git a/packages/api/src/router/widgets/minecraft.ts b/packages/api/src/router/widgets/minecraft.ts index 7f646c3bc..9cc5e25d7 100644 --- a/packages/api/src/router/widgets/minecraft.ts +++ b/packages/api/src/router/widgets/minecraft.ts @@ -1,7 +1,5 @@ -import { observable } from "@trpc/server/observable"; import { z } from "zod/v4"; -import type { MinecraftServerStatus } from "@homarr/request-handler/minecraft-server-status"; import { minecraftServerStatusRequestHandler } from "@homarr/request-handler/minecraft-server-status"; import { createTRPCRouter, publicProcedure } from "../../trpc"; @@ -16,21 +14,6 @@ export const minecraftRouter = createTRPCRouter({ isBedrockServer: input.isBedrockServer, domain: input.domain, }); - return await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true }); - }), - subscribeServerStatus: publicProcedure.input(serverStatusInputSchema).subscription(({ input }) => { - return observable((emit) => { - const innerHandler = minecraftServerStatusRequestHandler.handler({ - isBedrockServer: input.isBedrockServer, - domain: input.domain, - }); - const unsubscribe = innerHandler.subscribe((data) => { - emit.next(data); - }); - - return () => { - unsubscribe(); - }; - }); + return await innerHandler.getDataAsync(); }), }); diff --git a/packages/api/src/router/widgets/network-controller.ts b/packages/api/src/router/widgets/network-controller.ts index 053bd0dcb..61257d1ec 100644 --- a/packages/api/src/router/widgets/network-controller.ts +++ b/packages/api/src/router/widgets/network-controller.ts @@ -1,61 +1,26 @@ -import { observable } from "@trpc/server/observable"; - -import type { Modify } from "@homarr/common/types"; -import type { Integration } from "@homarr/db/schema"; -import type { IntegrationKindByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; -import type { NetworkControllerSummary } from "@homarr/integrations/types"; import { networkControllerRequestHandler } from "@homarr/request-handler/network-controller"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; export const networkControllerRouter = createTRPCRouter({ summary: publicProcedure .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("networkController"))) .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = networkControllerRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const innerHandler = networkControllerRequestHandler.handler(integration, {}); + const { data, timestamp } = await innerHandler.getDataAsync(); - return { - integration: { - id: integration.id, - name: integration.name, - kind: integration.kind, - }, - summary: data, - updatedAt: timestamp, - }; - }), - ); - return results; - }), - - subscribeToSummary: publicProcedure - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("networkController"))) - .subscription(({ ctx }) => { - return observable<{ - integration: Modify }>; - summary: NetworkControllerSummary; - }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integrationWithSecrets of ctx.integrations) { - const { decryptedSecrets: _, ...integration } = integrationWithSecrets; - const innerHandler = networkControllerRequestHandler.handler(integrationWithSecrets, {}); - const unsubscribe = innerHandler.subscribe((summary) => { - emit.next({ - integration, - summary, - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); + return { + integration: { + id: integration.id, + name: integration.name, + kind: integration.kind, + }, + summary: data, + updatedAt: timestamp, }; }); }), diff --git a/packages/api/src/router/widgets/notifications.ts b/packages/api/src/router/widgets/notifications.ts index 89ec95165..bae8ba9fd 100644 --- a/packages/api/src/router/widgets/notifications.ts +++ b/packages/api/src/router/widgets/notifications.ts @@ -1,14 +1,9 @@ -import { observable } from "@trpc/server/observable"; - -import type { Modify } from "@homarr/common/types"; -import type { Integration } from "@homarr/db/schema"; -import type { IntegrationKindByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; -import type { Notification } from "@homarr/integrations"; import { notificationsRequestHandler } from "@homarr/request-handler/notifications"; import type { IntegrationAction } from "../../middlewares/integration"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; const createNotificationsIntegrationMiddleware = (action: IntegrationAction) => @@ -18,46 +13,18 @@ export const notificationsRouter = createTRPCRouter({ getNotifications: publicProcedure .unstable_concat(createNotificationsIntegrationMiddleware("query")) .query(async ({ ctx }) => { - return await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = notificationsRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const innerHandler = notificationsRequestHandler.handler(integration, {}); + const { data, timestamp } = await innerHandler.getDataAsync(); - return { - integration: { - id: integration.id, - name: integration.name, - kind: integration.kind, - updatedAt: timestamp, - }, - data, - }; - }), - ); - }), - subscribeNotifications: publicProcedure - .unstable_concat(createNotificationsIntegrationMiddleware("query")) - .subscription(({ ctx }) => { - return observable<{ - integration: Modify }>; - data: Notification[]; - }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integrationWithSecrets of ctx.integrations) { - const { decryptedSecrets: _, ...integration } = integrationWithSecrets; - const innerHandler = notificationsRequestHandler.handler(integrationWithSecrets, {}); - const unsubscribe = innerHandler.subscribe((data) => { - emit.next({ - integration, - data, - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); + return { + integration: { + id: integration.id, + name: integration.name, + kind: integration.kind, + updatedAt: timestamp, + }, + data, }; }); }), diff --git a/packages/api/src/router/widgets/paperless-ngx.ts b/packages/api/src/router/widgets/paperless-ngx.ts index 0fb81e72a..2a1bf40de 100644 --- a/packages/api/src/router/widgets/paperless-ngx.ts +++ b/packages/api/src/router/widgets/paperless-ngx.ts @@ -6,7 +6,7 @@ import { createTRPCRouter, publicProcedure } from "../../trpc"; export const paperlessNgxRouter = createTRPCRouter({ getStats: publicProcedure.concat(createOneIntegrationMiddleware("query", "paperlessNgx")).query(async ({ ctx }) => { const innerHandler = paperlessNgxStatsRequestHandler.handler(ctx.integration, {}); - const data = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const data = await innerHandler.getDataAsync(); return data.data; }), }); diff --git a/packages/api/src/router/widgets/patchmon.ts b/packages/api/src/router/widgets/patchmon.ts new file mode 100644 index 000000000..aee758823 --- /dev/null +++ b/packages/api/src/router/widgets/patchmon.ts @@ -0,0 +1,21 @@ +import { patchmonStatsRequestHandler } from "@homarr/request-handler/patchmon"; + +import { createOneIntegrationMiddleware } from "../../middlewares/integration"; +import { createTRPCRouter, protectedProcedure } from "../../trpc"; + +export const patchmonRouter = createTRPCRouter({ + getStats: protectedProcedure + .meta({ + mcp: { + enabled: true, + description: + "Get PatchMon host patch statistics including total hosts, hosts needing updates, security update counts, up-to-date hosts, outdated packages, repositories, and OS distribution. REQUIRED: integrationId (single PatchMon integration ID from integration_all). Requires authenticated session (API key or browser login) and integration query access.", + }, + }) + .concat(createOneIntegrationMiddleware("query", "patchmon")) + .query(async ({ ctx }) => { + const innerHandler = patchmonStatsRequestHandler.handler(ctx.integration, {}); + const { data } = await innerHandler.getDataAsync(); + return data; + }), +}); diff --git a/packages/api/src/router/widgets/releases.ts b/packages/api/src/router/widgets/releases.ts index 3d355b26c..84e903627 100644 --- a/packages/api/src/router/widgets/releases.ts +++ b/packages/api/src/router/widgets/releases.ts @@ -1,11 +1,15 @@ +import SuperJSON from "superjson"; import { escapeForRegEx } from "@tiptap/react"; import { z } from "zod/v4"; -import { getIntegrationKindsByCategory } from "@homarr/definitions"; +import { decryptSecret } from "@homarr/common/server"; +import { eq } from "@homarr/db"; +import { boards, items, widgetSecrets } from "@homarr/db/schema"; +import { releaseProviderKinds } from "@homarr/definitions"; import { releasesRequestHandler } from "@homarr/request-handler/releases"; -import { createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createTRPCRouter, publicProcedure } from "../../trpc"; +import { throwIfActionForbiddenAsync } from "../board/board-access"; const formatVersionFilterRegex = (versionFilter: z.infer | undefined) => { if (!versionFilter) return undefined; @@ -25,37 +29,89 @@ const releaseVersionFilterSchema = z.object({ export const releasesRouter = createTRPCRouter({ getLatest: publicProcedure - .concat(createOneIntegrationMiddleware("query", ...getIntegrationKindsByCategory("releasesProvider"))) .input( z.object({ + itemId: z.string().optional(), repositories: z.array( z.object({ - id: z.string(), + id: z.string().optional(), + provider: z.enum(releaseProviderKinds), identifier: z.string(), versionFilter: releaseVersionFilterSchema.optional(), + providerUrl: z.string().url().optional(), }), ), }), ) .query(async ({ ctx, input }) => { + const tokensByProvider = new Map(); + const savedRepos = new Map(); + + if (input.itemId) { + try { + const item = await ctx.db.query.items.findFirst({ where: eq(items.id, input.itemId) }); + if (item && item.kind === "releases") { + await throwIfActionForbiddenAsync(ctx, eq(boards.id, item.boardId), "view"); + + const options = SuperJSON.parse>(item.options); + const repos = options.repositories as + | Array<{ provider?: string; identifier: string; providerUrl?: string }> + | undefined; + if (repos) { + for (const repo of repos) { + if (repo.provider && repo.identifier) { + savedRepos.set(`${repo.provider}:${repo.identifier}`, { providerUrl: repo.providerUrl }); + } + } + } + + const secrets = await ctx.db.query.widgetSecrets.findMany({ + where: eq(widgetSecrets.itemId, input.itemId), + }); + for (const secret of secrets) { + try { + tokensByProvider.set(secret.kind, decryptSecret(secret.value)); + } catch { + // Skip corrupt secrets + } + } + } + } catch { + // Access denied or table not yet migrated -- proceed without tokens + } + } + return await Promise.all( input.repositories.map(async (repository) => { - const response = await releasesRequestHandler - .handler(ctx.integration, { - id: repository.id, - identifier: repository.identifier, - versionRegex: formatVersionFilterRegex(repository.versionFilter), - }) - .getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); + const repositoryId = repository.id ?? repository.identifier; + const savedRepo = savedRepos.get(`${repository.provider}:${repository.identifier}`); + try { + const response = await releasesRequestHandler + .handler({ + id: repositoryId, + provider: repository.provider, + identifier: repository.identifier, + versionRegex: formatVersionFilterRegex(repository.versionFilter), + providerUrl: savedRepo?.providerUrl ?? repository.providerUrl, + token: savedRepo ? tokensByProvider.get(repository.provider) : undefined, + }) + .getDataAsync(); - return { - id: repository.id, - integration: { name: ctx.integration.name, kind: ctx.integration.kind }, - timestamp: response.timestamp, - ...response.data, - }; + return { + id: repositoryId, + provider: repository.provider, + timestamp: response.timestamp, + ...response.data, + }; + } catch (error) { + return { + id: repositoryId, + provider: repository.provider, + timestamp: new Date(), + success: false as const, + error: { code: "unexpected" as const, message: String(error) }, + }; + } }), ); }), diff --git a/packages/api/src/router/widgets/rssFeed.ts b/packages/api/src/router/widgets/rssFeed.ts index ec9c223e0..5e43efe3e 100644 --- a/packages/api/src/router/widgets/rssFeed.ts +++ b/packages/api/src/router/widgets/rssFeed.ts @@ -1,4 +1,3 @@ -import { observable } from "@trpc/server/observable"; import SuperJSON from "superjson"; import { z } from "zod/v4"; @@ -6,12 +5,14 @@ import type { Session } from "@homarr/auth"; import type { Database } from "@homarr/db"; import { eq } from "@homarr/db"; import { items, users } from "@homarr/db/schema"; -import type { ExtendedFeedEntry } from "@homarr/request-handler/rss-feeds"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds"; import type { WidgetComponentProps } from "../../../../widgets/src"; import { createTRPCRouter, publicProcedure } from "../../trpc"; +const logger = createLogger({ module: "rssFeed" }); + const feedsInput = z.object({ urls: z.array(z.string()).max(100), maximumAmountPosts: z.number(), @@ -23,41 +24,28 @@ export const rssFeedRouter = createTRPCRouter({ ? input.urls : await restrictUrlsAsync(ctx.db, input.urls); - const rssFeeds = await Promise.all( + const settled = await Promise.allSettled( urls.map(async (url) => { const innerHandler = rssFeedsRequestHandler.handler({ url, count: input.maximumAmountPosts, }); - return await innerHandler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); + return await innerHandler.getDataAsync(); }), ); - return rssFeeds - .flatMap((rssFeed) => rssFeed.data.entries) - .slice(0, input.maximumAmountPosts) - .sort((entryA, entryB) => { + return settled + .flatMap((result, index) => { + if (result.status === "fulfilled") return result.value.data.entries; + logger.warn("RSS feed fetch failed", { url: urls[index], error: result.reason }); + return []; + }) + .toSorted((entryA, entryB) => { return entryA.published && entryB.published ? new Date(entryB.published).getTime() - new Date(entryA.published).getTime() : 0; - }); - }), - subscribeFeeds: publicProcedure.input(feedsInput).subscription(({ input }) => { - // It is not necessary to check access to all feeds here, as the client will only subscribe to feeds which already exist - return observable<{ url: string; entries: ExtendedFeedEntry[] }>((emit) => { - const unsubscribes = input.urls.map((url) => { - const innerHandler = rssFeedsRequestHandler.handler({ url, count: input.maximumAmountPosts }); - return innerHandler.subscribe((data) => { - emit.next({ url, entries: data.entries }); - }); - }); - - return () => { - unsubscribes.forEach((unsubscribe) => unsubscribe()); - }; - }); + }) + .slice(0, input.maximumAmountPosts); }), }); diff --git a/packages/api/src/router/widgets/smart-home.ts b/packages/api/src/router/widgets/smart-home.ts index dec3770a7..c37468752 100644 --- a/packages/api/src/router/widgets/smart-home.ts +++ b/packages/api/src/router/widgets/smart-home.ts @@ -1,4 +1,3 @@ -import { observable } from "@trpc/server/observable"; import { z } from "zod/v4"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; @@ -25,31 +24,9 @@ export const smartHomeRouter = createTRPCRouter({ .concat(createSmartHomeIntegrationMiddleware("query")) .query(async ({ ctx: { integration }, input }) => { const innerHandler = smartHomeEntityStateRequestHandler.handler(integration, { entityId: input.entityId }); - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); + const { data } = await innerHandler.getDataAsync(); return data; }), - subscribeEntityState: publicProcedure - .concat(createSmartHomeIntegrationMiddleware("query")) - .input(z.object({ entityId: z.string() })) - .subscription(({ input, ctx }) => { - return observable<{ - entityId: string; - state: string; - }>((emit) => { - const innerHandler = smartHomeEntityStateRequestHandler.handler(ctx.integration, { - entityId: input.entityId, - }); - const unsubscribe = innerHandler.subscribe((state) => { - emit.next({ state, entityId: input.entityId }); - }); - - return () => { - unsubscribe(); - }; - }); - }), switchEntity: protectedProcedure .meta({ mcp: { @@ -64,9 +41,6 @@ export const smartHomeRouter = createTRPCRouter({ const client = await createIntegrationAsync(integration); const success = await client.triggerToggleAsync(input.entityId); - const innerHandler = smartHomeEntityStateRequestHandler.handler(integration, { entityId: input.entityId }); - await innerHandler.invalidateAsync(); - return success; }), executeAutomation: protectedProcedure diff --git a/packages/api/src/router/widgets/speedtest-tracker.ts b/packages/api/src/router/widgets/speedtest-tracker.ts index be4245921..01e7fb58b 100644 --- a/packages/api/src/router/widgets/speedtest-tracker.ts +++ b/packages/api/src/router/widgets/speedtest-tracker.ts @@ -1,54 +1,23 @@ -import { observable } from "@trpc/server/observable"; - -import type { SpeedtestTrackerDashboardData } from "@homarr/integrations/types"; import { speedtestTrackerRequestHandler } from "@homarr/request-handler/speedtest-tracker"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; export const speedtestTrackerRouter = createTRPCRouter({ getDashboard: publicProcedure .concat(createManyIntegrationMiddleware("query", "speedtestTracker")) .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = speedtestTrackerRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const innerHandler = speedtestTrackerRequestHandler.handler(integration, {}); + const { data, timestamp } = await innerHandler.getDataAsync(); - return { - integrationId: integration.id, - integrationName: integration.name, - integrationUrl: integration.url, - dashboard: data, - updatedAt: timestamp, - }; - }), - ); - - return results; - }), - - subscribeToDashboard: publicProcedure - .concat(createManyIntegrationMiddleware("query", "speedtestTracker")) - .subscription(({ ctx }) => { - return observable<{ - integrationId: string; - dashboard: SpeedtestTrackerDashboardData; - timestamp: Date; - }>((emit) => { - const unsubscribes = ctx.integrations.map((integration) => { - const innerHandler = speedtestTrackerRequestHandler.handler(integration, {}); - return innerHandler.subscribe((dashboard) => { - emit.next({ - integrationId: integration.id, - dashboard, - timestamp: new Date(), - }); - }); - }); - - return () => { - unsubscribes.forEach((unsubscribe) => unsubscribe()); + return { + integrationId: integration.id, + integrationName: integration.name, + integrationUrl: integration.url, + dashboard: data, + updatedAt: timestamp, }; }); }), diff --git a/packages/api/src/router/widgets/stocks.ts b/packages/api/src/router/widgets/stocks.ts index d7b4ed13a..7fc08005b 100644 --- a/packages/api/src/router/widgets/stocks.ts +++ b/packages/api/src/router/widgets/stocks.ts @@ -18,6 +18,6 @@ export const stockPriceRouter = createTRPCRouter({ timeRange: input.timeRange, timeInterval: input.timeInterval, }); - return await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + return await innerHandler.getDataAsync(); }), }); diff --git a/packages/api/src/router/widgets/timetable.ts b/packages/api/src/router/widgets/timetable.ts index 787be4c37..ef60ce320 100644 --- a/packages/api/src/router/widgets/timetable.ts +++ b/packages/api/src/router/widgets/timetable.ts @@ -1,43 +1,36 @@ import z from "zod"; -import { getIntegrationKindsByCategory } from "@homarr/definitions"; -import { timetableOptionsSchema } from "@homarr/integrations/types"; import { timetableGetTimetableRequestHandler, timetableSearchStationsRequestHandler, } from "@homarr/request-handler/timetable"; -import { createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createTRPCRouter, publicProcedure } from "../../trpc"; +const baseUrlSchema = z.string().url(); + export const timetableRouter = createTRPCRouter({ getTimetable: publicProcedure - .concat(createOneIntegrationMiddleware("query", ...getIntegrationKindsByCategory("timetable"))) - .input(timetableOptionsSchema) - .query(async ({ input, ctx }) => { - const innerHandler = timetableGetTimetableRequestHandler.handler(ctx.integration, { - stationId: input.stationId, - limit: input.limit, - }); - - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); - + .input( + z.object({ + baseUrl: baseUrlSchema, + stationId: z.string(), + limit: z.number().int().min(1).max(100), + }), + ) + .query(async ({ input }) => { + const { data } = await timetableGetTimetableRequestHandler.handler(input).getDataAsync(); return data; }), searchStations: publicProcedure - .concat(createOneIntegrationMiddleware("query", ...getIntegrationKindsByCategory("timetable"))) .input( z.object({ + baseUrl: baseUrlSchema, query: z.string(), }), ) - .query(async ({ input, ctx }) => { - const innerHandler = timetableSearchStationsRequestHandler.handler(ctx.integration, { - query: input.query, - }); - - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); - + .query(async ({ input }) => { + const { data } = await timetableSearchStationsRequestHandler.handler(input).getDataAsync(); return data; }), }); diff --git a/packages/api/src/router/widgets/tracearr.ts b/packages/api/src/router/widgets/tracearr.ts index d46c80c66..5075c990b 100644 --- a/packages/api/src/router/widgets/tracearr.ts +++ b/packages/api/src/router/widgets/tracearr.ts @@ -1,48 +1,22 @@ -import { observable } from "@trpc/server/observable"; - -import type { TracearrDashboardData } from "@homarr/integrations/types"; import { tracearrRequestHandler } from "@homarr/request-handler/tracearr"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; export const tracearrRouter = createTRPCRouter({ getDashboard: publicProcedure.concat(createManyIntegrationMiddleware("query", "tracearr")).query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = tracearrRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const innerHandler = tracearrRequestHandler.handler(integration, {}); + const { data, timestamp } = await innerHandler.getDataAsync(); - return { - integrationId: integration.id, - integrationName: integration.name, - integrationUrl: integration.url, - dashboard: data, - updatedAt: timestamp, - }; - }), - ); - - return results; + return { + integrationId: integration.id, + integrationName: integration.name, + integrationUrl: integration.url, + dashboard: data, + updatedAt: timestamp, + }; + }); }), - subscribeToDashboard: publicProcedure - .concat(createManyIntegrationMiddleware("query", "tracearr")) - .subscription(({ ctx }) => { - return observable<{ integrationId: string; dashboard: TracearrDashboardData; timestamp: Date }>((emit) => { - const unsubscribes = ctx.integrations.map((integration) => { - const innerHandler = tracearrRequestHandler.handler(integration, {}); - return innerHandler.subscribe((dashboard) => { - emit.next({ - integrationId: integration.id, - dashboard, - timestamp: new Date(), - }); - }); - }); - - return () => { - unsubscribes.forEach((unsubscribe) => unsubscribe()); - }; - }); - }), }); diff --git a/packages/api/src/router/widgets/traefik.ts b/packages/api/src/router/widgets/traefik.ts new file mode 100644 index 000000000..8f9c775e2 --- /dev/null +++ b/packages/api/src/router/widgets/traefik.ts @@ -0,0 +1,31 @@ +import { traefikRequestHandler } from "@homarr/request-handler/traefik"; + +import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; +import { createTRPCRouter, publicProcedure } from "../../trpc"; + +export const traefikRouter = createTRPCRouter({ + getDashboard: publicProcedure + .meta({ + mcp: { + enabled: true, + description: + "Retrieve Traefik dashboard data, including routers, services, middlewares, and entry points, for the given integration IDs.", + }, + }) + .concat(createManyIntegrationMiddleware("query", "traefik")) + .query(async ({ ctx }) => { + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const innerHandler = traefikRequestHandler.handler(integration, {}); + const { data, timestamp } = await innerHandler.getDataAsync(); + + return { + integrationId: integration.id, + integrationName: integration.name, + integrationUrl: integration.url, + dashboard: data, + updatedAt: timestamp, + }; + }); + }), +}); diff --git a/packages/api/src/router/widgets/umami.ts b/packages/api/src/router/widgets/umami.ts index 2c240f4bb..3994f398e 100644 --- a/packages/api/src/router/widgets/umami.ts +++ b/packages/api/src/router/widgets/umami.ts @@ -1,4 +1,3 @@ -import { observable } from "@trpc/server/observable"; import { z } from "zod/v4"; import { createLogger } from "@homarr/core/infrastructure/logs"; @@ -14,6 +13,7 @@ import { } from "@homarr/request-handler/umami"; import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; const logger = createLogger({ module: "umami-router" }); @@ -39,26 +39,22 @@ export const umamiRouter = createTRPCRouter({ ) .concat(createManyIntegrationMiddleware("query", "umami")) .query(async ({ ctx, input }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = umamiRequestHandler.handler(integration, { - websiteId: input.websiteId, - timeFrame: input.timeFrame, - eventName: input.eventName, - }); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const innerHandler = umamiRequestHandler.handler(integration, { + websiteId: input.websiteId, + timeFrame: input.timeFrame, + eventName: input.eventName, + }); + const { data, timestamp } = await innerHandler.getDataAsync(); - return { - integrationId: integration.id, - integrationName: integration.name, - integrationUrl: integration.url, - visitorStats: data, - updatedAt: timestamp, - }; - }), - ); - - return results; + return { + integrationId: integration.id, + integrationName: integration.name, + integrationUrl: integration.url, + visitorStats: data, + updatedAt: timestamp, + }; + }); }), getEventNames: publicProcedure @@ -67,7 +63,7 @@ export const umamiRouter = createTRPCRouter({ .query(async ({ ctx, input }) => { try { const innerHandler = umamiEventNamesRequestHandler.handler(ctx.integration, { websiteId: input.websiteId }); - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data } = await innerHandler.getDataAsync(); return data; } catch (error) { logger.warn( @@ -93,7 +89,7 @@ export const umamiRouter = createTRPCRouter({ timeFrame: input.timeFrame, limit: input.limit, }); - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data } = await innerHandler.getDataAsync(); return data; } catch (error) { logger.warn( @@ -123,7 +119,7 @@ export const umamiRouter = createTRPCRouter({ timeFrame: input.timeFrame, limit: input.limit, }); - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data } = await innerHandler.getDataAsync(); return data; } catch (error) { logger.warn( @@ -148,13 +144,13 @@ export const umamiRouter = createTRPCRouter({ .concat(createOneIntegrationMiddleware("query", "umami")) .query(async ({ ctx, input }) => { try { - const sortedNames = [...input.eventNames].sort(); + const sortedNames = [...input.eventNames].toSorted(); const innerHandler = umamiMultiEventRequestHandler.handler(ctx.integration, { websiteId: input.websiteId, timeFrame: input.timeFrame, eventNames: sortedNames, }); - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data } = await innerHandler.getDataAsync(); return data; } catch (error) { logger.warn( @@ -176,7 +172,7 @@ export const umamiRouter = createTRPCRouter({ const innerHandler = umamiActiveVisitorsRequestHandler.handler(ctx.integration, { websiteId: input.websiteId, }); - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data } = await innerHandler.getDataAsync(); return data; } catch (error) { logger.warn( @@ -185,21 +181,4 @@ export const umamiRouter = createTRPCRouter({ return 0; } }), - - subscribeActiveVisitors: publicProcedure - .input(z.object({ websiteId: z.string() })) - .concat(createOneIntegrationMiddleware("query", "umami")) - .subscription(({ ctx, input }) => { - return observable((emit) => { - const innerHandler = umamiActiveVisitorsRequestHandler.handler(ctx.integration, { - websiteId: input.websiteId, - }); - const unsubscribe = innerHandler.subscribe((count) => { - emit.next(count); - }); - return () => { - unsubscribe(); - }; - }); - }), }); diff --git a/packages/api/src/router/widgets/ups.ts b/packages/api/src/router/widgets/ups.ts index f1c27c434..16c6312dd 100644 --- a/packages/api/src/router/widgets/ups.ts +++ b/packages/api/src/router/widgets/ups.ts @@ -1,50 +1,24 @@ -import { observable } from "@trpc/server/observable"; - import { getIntegrationKindsByCategory } from "@homarr/definitions"; -import type { UpsSummary } from "@homarr/integrations/types"; import { upsSummariesRequestHandler } from "@homarr/request-handler/ups"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; export const upsRouter = createTRPCRouter({ getSummaries: publicProcedure .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("ups"))) .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = upsSummariesRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const innerHandler = upsSummariesRequestHandler.handler(integration, {}); + const { data, timestamp } = await innerHandler.getDataAsync(); - return { - integrationId: integration.id, - integrationName: integration.name, - integrationUrl: integration.url, - summaries: data, - updatedAt: timestamp, - }; - }), - ); - - return results; - }), - subscribeSummaries: publicProcedure - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("ups"))) - .subscription(({ ctx }) => { - return observable<{ integrationId: string; summaries: UpsSummary[]; timestamp: Date }>((emit) => { - const unsubscribes = ctx.integrations.map((integration) => { - const innerHandler = upsSummariesRequestHandler.handler(integration, {}); - return innerHandler.subscribe((summaries) => { - emit.next({ - integrationId: integration.id, - summaries, - timestamp: new Date(), - }); - }); - }); - - return () => { - unsubscribes.forEach((unsubscribe) => unsubscribe()); + return { + integrationId: integration.id, + integrationName: integration.name, + integrationUrl: integration.url, + summaries: data, + updatedAt: timestamp, }; }); }), diff --git a/packages/api/src/router/widgets/uptime-kuma.ts b/packages/api/src/router/widgets/uptime-kuma.ts index eb28f9e7e..6df14090c 100644 --- a/packages/api/src/router/widgets/uptime-kuma.ts +++ b/packages/api/src/router/widgets/uptime-kuma.ts @@ -1,54 +1,23 @@ -import { observable } from "@trpc/server/observable"; - -import type { UptimeKumaDashboardData } from "@homarr/integrations/types"; import { uptimeKumaRequestHandler } from "@homarr/request-handler/uptime-kuma"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; export const uptimeKumaRouter = createTRPCRouter({ getDashboard: publicProcedure .concat(createManyIntegrationMiddleware("query", "uptimeKuma")) .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = uptimeKumaRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const innerHandler = uptimeKumaRequestHandler.handler(integration, {}); + const { data, timestamp } = await innerHandler.getDataAsync(); - return { - integrationId: integration.id, - integrationName: integration.name, - integrationUrl: integration.url, - dashboard: data, - updatedAt: timestamp, - }; - }), - ); - - return results; - }), - - subscribeToDashboard: publicProcedure - .concat(createManyIntegrationMiddleware("query", "uptimeKuma")) - .subscription(({ ctx }) => { - return observable<{ - integrationId: string; - dashboard: UptimeKumaDashboardData; - timestamp: Date; - }>((emit) => { - const unsubscribes = ctx.integrations.map((integration) => { - const innerHandler = uptimeKumaRequestHandler.handler(integration, {}); - return innerHandler.subscribe((dashboard) => { - emit.next({ - integrationId: integration.id, - dashboard, - timestamp: new Date(), - }); - }); - }); - - return () => { - unsubscribes.forEach((unsubscribe) => unsubscribe()); + return { + integrationId: integration.id, + integrationName: integration.name, + integrationUrl: integration.url, + dashboard: data, + updatedAt: timestamp, }; }); }), diff --git a/packages/api/src/router/widgets/vpn.ts b/packages/api/src/router/widgets/vpn.ts index f50d65f33..407e4986f 100644 --- a/packages/api/src/router/widgets/vpn.ts +++ b/packages/api/src/router/widgets/vpn.ts @@ -1,25 +1,16 @@ -import { observable } from "@trpc/server/observable"; - -import type { Modify } from "@homarr/common/types"; -import type { Integration } from "@homarr/db/schema"; -import type { IntegrationKindByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; -import type { VpnSummary } from "@homarr/integrations/types"; import { vpnSummaryHandler } from "@homarr/request-handler/vpn"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; -const createVpnIntegrationMiddleware = () => - createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("vpn")); - export const vpnRouter = createTRPCRouter({ - getSummaries: publicProcedure.unstable_concat(createVpnIntegrationMiddleware()).query(async ({ ctx }) => { - return await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = vpnSummaryHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); - + getSummaries: publicProcedure + .unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("vpn"))) + .query(async ({ ctx }) => { + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const { data, timestamp } = await vpnSummaryHandler.handler(integration, {}).getDataAsync(); return { integration: { id: integration.id, @@ -29,31 +20,6 @@ export const vpnRouter = createTRPCRouter({ }, summary: data, }; - }), - ); - }), - subscribeSummaries: publicProcedure.unstable_concat(createVpnIntegrationMiddleware()).subscription(({ ctx }) => { - return observable<{ - integration: Modify }>; - summary: VpnSummary | null; - }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integrationWithSecrets of ctx.integrations) { - const { decryptedSecrets: _, ...integration } = integrationWithSecrets; - const innerHandler = vpnSummaryHandler.handler(integrationWithSecrets, {}); - const unsubscribe = innerHandler.subscribe((summary) => { - emit.next({ - integration, - summary, - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); - }; - }); - }), + }); + }), }); diff --git a/packages/api/src/router/widgets/weather.ts b/packages/api/src/router/widgets/weather.ts index bb1077569..87ea6b48a 100644 --- a/packages/api/src/router/widgets/weather.ts +++ b/packages/api/src/router/widgets/weather.ts @@ -1,7 +1,5 @@ -import { observable } from "@trpc/server/observable"; import { z } from "zod/v4"; -import type { Weather } from "@homarr/request-handler/weather"; import { weatherRequestHandler } from "@homarr/request-handler/weather"; import { createTRPCRouter, publicProcedure } from "../../trpc"; @@ -14,16 +12,6 @@ const atLocationInput = z.object({ export const weatherRouter = createTRPCRouter({ atLocation: publicProcedure.input(atLocationInput).query(async ({ input }) => { const handler = weatherRequestHandler.handler(input); - return await handler.getCachedOrUpdatedDataAsync({ forceUpdate: false }).then((result) => result.data); - }), - subscribeAtLocation: publicProcedure.input(atLocationInput).subscription(({ input }) => { - return observable((emit) => { - const handler = weatherRequestHandler.handler(input); - const unsubscribe = handler.subscribe((data) => { - emit.next(data); - }); - - return unsubscribe; - }); + return await handler.getDataAsync().then((result) => result.data); }), }); diff --git a/packages/api/src/router/widgets/widget-secrets.ts b/packages/api/src/router/widgets/widget-secrets.ts new file mode 100644 index 000000000..75ae23cca --- /dev/null +++ b/packages/api/src/router/widgets/widget-secrets.ts @@ -0,0 +1,97 @@ +import { TRPCError } from "@trpc/server"; +import { z } from "zod/v4"; + +import { encryptSecret } from "@homarr/common/server"; +import { and, eq, handleTransactionsAsync } from "@homarr/db"; +import { boards, items, widgetSecrets } from "@homarr/db/schema"; +import { releaseProviderKinds } from "@homarr/definitions"; + +import { createTRPCRouter, protectedProcedure } from "../../trpc"; +import { throwIfActionForbiddenAsync } from "../board/board-access"; + +const secretKindSchema = z.enum(releaseProviderKinds); + +const resolveItemWithBoardAccess = async (ctx: { db: any; session: any }, itemId: string) => { + const item = await ctx.db.query.items.findFirst({ where: eq(items.id, itemId) }); + if (!item || item.kind !== "releases") throw new TRPCError({ code: "NOT_FOUND" }); + await throwIfActionForbiddenAsync(ctx, eq(boards.id, item.boardId), "modify"); + return item; +}; + +export const widgetSecretsRouter = createTRPCRouter({ + setSecret: protectedProcedure + .meta({ + mcp: { + enabled: true, + description: + "Set an authentication token for a release provider on a specific releases widget. Requires modify permission on the widget's board. Use widget_secrets_getConfiguredKinds to check existing tokens", + }, + }) + .input(z.object({ itemId: z.string(), kind: secretKindSchema, value: z.string().min(1).max(4096) })) + .mutation(async ({ ctx, input }) => { + await resolveItemWithBoardAccess(ctx, input.itemId); + + const encrypted = encryptSecret(input.value); + const row = { itemId: input.itemId, kind: input.kind, value: encrypted, updatedAt: new Date() }; + + await handleTransactionsAsync(ctx.db, { + async handleAsync(db, schema) { + await db.transaction(async (tx) => { + await tx + .delete(schema.widgetSecrets) + .where(and(eq(schema.widgetSecrets.itemId, input.itemId), eq(schema.widgetSecrets.kind, input.kind))); + await tx.insert(schema.widgetSecrets).values(row); + }); + }, + handleSync(db) { + db.transaction((tx) => { + tx.delete(widgetSecrets) + .where(and(eq(widgetSecrets.itemId, input.itemId), eq(widgetSecrets.kind, input.kind))) + .run(); + tx.insert(widgetSecrets).values(row).run(); + }); + }, + }); + }), + + deleteSecret: protectedProcedure + .meta({ + mcp: { + enabled: true, + description: + "Remove an authentication token for a release provider from a specific releases widget. Requires modify permission on the widget's board", + }, + }) + .input(z.object({ itemId: z.string(), kind: secretKindSchema })) + .mutation(async ({ ctx, input }) => { + await resolveItemWithBoardAccess(ctx, input.itemId); + + await ctx.db + .delete(widgetSecrets) + .where(and(eq(widgetSecrets.itemId, input.itemId), eq(widgetSecrets.kind, input.kind))); + }), + + getConfiguredKinds: protectedProcedure + .meta({ + mcp: { + enabled: true, + description: + "List which release provider kinds have tokens configured for a releases widget. Returns an array of provider kind strings (e.g. github, gitlab, dockerHub). Requires modify permission on the widget's board", + }, + }) + .input(z.object({ itemId: z.string() })) + .query(async ({ ctx, input }) => { + await resolveItemWithBoardAccess(ctx, input.itemId); + + try { + const secrets = await ctx.db.query.widgetSecrets.findMany({ + where: eq(widgetSecrets.itemId, input.itemId), + columns: { kind: true }, + }); + return secrets.map((s: { kind: string }) => s.kind); + } catch { + // ponytail: table may not exist pre-migration, return empty + return [] as string[]; + } + }), +}); diff --git a/packages/api/src/services/duckduckgo-bangs.ts b/packages/api/src/services/duckduckgo-bangs.ts index 297f5c8ed..48de45dd6 100644 --- a/packages/api/src/services/duckduckgo-bangs.ts +++ b/packages/api/src/services/duckduckgo-bangs.ts @@ -39,7 +39,7 @@ export const searchDuckDuckGoBangsAsync = async (input: { const queryTokenPrefix = normalizeBangToken(input.query); if (!queryTokenPrefix) return []; - const { data: allBangs } = await duckDuckGoBangsRequestHandler.handler({}).getCachedOrUpdatedDataAsync({}); + const { data: allBangs } = await duckDuckGoBangsRequestHandler.handler({}).getDataAsync(); const startIndex = lowerBound(allBangs, queryTokenPrefix); const matches: DuckDuckGoBang[] = []; diff --git a/packages/api/src/settle-integrations.ts b/packages/api/src/settle-integrations.ts new file mode 100644 index 000000000..643a3b949 --- /dev/null +++ b/packages/api/src/settle-integrations.ts @@ -0,0 +1,35 @@ +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; + +const logger = createLogger({ module: "settleIntegrations" }); + +interface IntegrationLike { + id: string; + name: string; + kind: string; +} + +interface Options { + fallback?: (integration: TIntegration, error: unknown) => TResult; +} + +export async function settleIntegrationQueries( + integrations: TIntegration[], + fn: (integration: TIntegration) => Promise, + options?: Options, +): Promise { + const settled = await Promise.allSettled(integrations.map(async (integration) => fn(integration))); + return settled.flatMap((result, index) => { + if (result.status === "fulfilled") return [result.value]; + const integration = integrations[index]; + logger.warn( + new ErrorWithMetadata( + "Integration query failed", + { integrationId: integration?.id, integrationKind: integration?.kind }, + { cause: result.reason }, + ), + ); + if (options?.fallback && integration) return [options.fallback(integration, result.reason)]; + return []; + }); +} diff --git a/packages/api/src/test/mcp.spec.ts b/packages/api/src/test/mcp.spec.ts index 4fc696aa7..e39e0e4ab 100644 --- a/packages/api/src/test/mcp.spec.ts +++ b/packages/api/src/test/mcp.spec.ts @@ -2,16 +2,20 @@ import { expect, test, vi } from "vitest"; import { extractToolsFromProcedures } from "trpc-to-mcp"; import { appRouter } from "../router/app"; +import { boardRouter } from "../router/board"; import { infoRouter } from "../router/info"; import { inviteRouter } from "../router/invite"; +import { serverSettingsRouter } from "../router/serverSettings"; import { createTRPCRouter } from "../trpc"; vi.mock("@homarr/auth", () => ({})); const mcpTestRouter = createTRPCRouter({ app: appRouter, + board: boardRouter, info: infoRouter, invite: inviteRouter, + serverSettings: serverSettingsRouter, }); test("MCP tools should be extractable from the router", () => { @@ -28,8 +32,12 @@ test("MCP tools should contain expected procedures", () => { expect(toolNames).toContain("app_all"); expect(toolNames).toContain("app_byId"); expect(toolNames).toContain("app_create"); + expect(toolNames).toContain("board_savePartialBoardSettings"); + expect(toolNames).toContain("board_duplicateBoard"); expect(toolNames).toContain("info_getInfo"); expect(toolNames).toContain("invite_getAll"); + expect(toolNames).toContain("serverSettings_getBoardSettings"); + expect(toolNames).toContain("serverSettings_updateBoardSettings"); }); test("MCP tools should have descriptions", () => { diff --git a/packages/api/src/test/open-api.spec.ts b/packages/api/src/test/open-api.spec.ts index e7d240564..a89327ce3 100644 --- a/packages/api/src/test/open-api.spec.ts +++ b/packages/api/src/test/open-api.spec.ts @@ -14,3 +14,12 @@ test("OpenAPI documentation should be generated", () => { // Assert expect(act).not.toThrow(); }); + +test("OpenAPI documentation should expose board automation endpoints", () => { + const document = openApiDocument("https://homarr.dev"); + + expect(document.info.version).toBe("1.1.0"); + expect(document.paths).toHaveProperty("/api/boards/{id}/settings"); + expect(document.paths).toHaveProperty("/api/boards/{id}/duplicate"); + expect(document.paths).toHaveProperty("/api/settings/board"); +}); diff --git a/packages/api/src/test/query-cache.spec.ts b/packages/api/src/test/query-cache.spec.ts index c765af031..68d28eb15 100644 --- a/packages/api/src/test/query-cache.spec.ts +++ b/packages/api/src/test/query-cache.spec.ts @@ -10,59 +10,31 @@ import { isPersistableWidgetQueryKey, queryCacheDefaultStaleTimeMs, queryCacheMaxValueBytes, - queryCacheRetentionMs, } from "@homarr/api/query-cache"; import { queryCacheRouter } from "../router/query-cache"; const redisMock = vi.hoisted(() => { - const values = new Map(); - const calls: { - userId: string; - boardId: string; - key: string; - ttlMs: number; - maxValueBytes: number; - }[] = []; + const values = new Map(); return { values, - calls, - createQueryCacheChannel: vi.fn( - (options: { userId: string; boardId: string; key: string; ttlMs: number; maxValueBytes: number }) => { - calls.push(options); - const storageKey = `${options.userId}:${options.boardId}:${options.key}`; - - return { - getAsync: async () => { - const item = values.get(storageKey); - if (!item) return null; - if (Date.now() > item.expiresAt) { - values.delete(storageKey); - return null; - } - - return item.value; - }, - setAsync: async (value: string) => { - if (Buffer.byteLength(value, "utf8") > options.maxValueBytes) { - return false; - } - - values.set(storageKey, { value, expiresAt: Date.now() + options.ttlMs }); - return true; - }, - removeAsync: async () => { - values.delete(storageKey); - }, - }; + setQueryCacheAsync: vi.fn( + async (_userId: string, _boardId: string, value: string, _ttlMs: number, maxValueBytes: number) => { + if (Buffer.byteLength(value, "utf8") > maxValueBytes) return false; + values.set(`${_userId}:${_boardId}`, value); + return true; }, ), + removeQueryCacheAsync: vi.fn(async (_userId: string, _boardId: string) => { + values.delete(`${_userId}:${_boardId}`); + }), }; }); vi.mock("@homarr/redis", () => ({ - createQueryCacheChannel: redisMock.createQueryCacheChannel, + setQueryCacheAsync: redisMock.setQueryCacheAsync, + removeQueryCacheAsync: redisMock.removeQueryCacheAsync, })); vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session })); @@ -70,8 +42,8 @@ vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session })); afterEach(() => { vi.useRealTimers(); redisMock.values.clear(); - redisMock.calls.length = 0; - redisMock.createQueryCacheChannel.mockClear(); + redisMock.setQueryCacheAsync.mockClear(); + redisMock.removeQueryCacheAsync.mockClear(); }); const createSession = (userId = createId()): Session => ({ @@ -95,6 +67,8 @@ describe("isPersistableWidgetQueryKey", () => { [[["docker", "getContainers"], { type: "query" }], true], [[["integration", "byIds"], { type: "query" }], true], [[["widget", "app", "ping"], { type: "query", input: { id: "abc" } }], false], + [[["widget", "beszel", "getSystemStats"], { type: "query" }], false], + [[["widget", "beszel", "getSystems"], { type: "query" }], true], [[["app", "selectable"], { type: "query" }], false], [[["board", "getBoardByName"], { type: "query" }], false], [["widget"], false], @@ -104,7 +78,7 @@ describe("isPersistableWidgetQueryKey", () => { }); describe("queryCacheRouter", () => { - test("stores, reads, and removes data for a board viewer", async () => { + test("stores and removes data for a board viewer", async () => { const db = createDb(); const session = createSession(); await insertUserAsync(db, session.user.id); @@ -112,14 +86,19 @@ describe("queryCacheRouter", () => { const caller = queryCacheRouter.createCaller({ db, deviceType: undefined, session }); - await expect(caller.setItem({ boardId: "board-1", key: "query-key", value: "cached-value" })).resolves.toEqual({ + await expect(caller.setItem({ boardId: "board-1", value: "cached-value" })).resolves.toEqual({ stored: true, }); - await expect(caller.getItem({ boardId: "board-1", key: "query-key" })).resolves.toBe("cached-value"); + expect(redisMock.setQueryCacheAsync).toHaveBeenCalledWith( + session.user.id, + "board-1", + "cached-value", + expect.any(Number), + queryCacheMaxValueBytes, + ); - await caller.removeItem({ boardId: "board-1", key: "query-key" }); - - await expect(caller.getItem({ boardId: "board-1", key: "query-key" })).resolves.toBeNull(); + await caller.removeItem({ boardId: "board-1" }); + expect(redisMock.removeQueryCacheAsync).toHaveBeenCalledWith(session.user.id, "board-1"); }); test("does not touch Redis when board access is denied", async () => { @@ -127,32 +106,32 @@ describe("queryCacheRouter", () => { await insertUserAsync(db, "owner"); await db.insert(boards).values({ id: "board-1", name: "board", creatorId: "owner", isPublic: false }); - redisMock.createQueryCacheChannel.mockClear(); + redisMock.setQueryCacheAsync.mockClear(); const caller = queryCacheRouter.createCaller({ db, deviceType: undefined, session: createSession("other-user") }); - await expect(caller.getItem({ boardId: "board-1", key: "query-key" })).rejects.toThrow("Board not found"); - expect(redisMock.createQueryCacheChannel).not.toHaveBeenCalled(); + await expect(caller.setItem({ boardId: "board-1", value: "cached-value" })).rejects.toThrow("Board not found"); + expect(redisMock.setQueryCacheAsync).not.toHaveBeenCalled(); }); - test("scopes anonymous public-board cache entries by anonymous user and board", async () => { + test("scopes anonymous public-board cache entries by anonymous user", async () => { const db = createDb(); await insertUserAsync(db, "owner"); await db.insert(boards).values({ id: "board-1", name: "board", creatorId: "owner", isPublic: true }); const caller = queryCacheRouter.createCaller({ db, deviceType: undefined, session: null }); - await caller.setItem({ boardId: "board-1", key: "query-key", value: "cached-value" }); + await caller.setItem({ boardId: "board-1", value: "cached-value" }); - expect(redisMock.calls.at(-1)).toMatchObject({ - userId: "anonymous", - boardId: "board-1", - key: "query-key", - ttlMs: queryCacheRetentionMs, - maxValueBytes: queryCacheMaxValueBytes, - }); + expect(redisMock.setQueryCacheAsync).toHaveBeenCalledWith( + "anonymous", + "board-1", + "cached-value", + expect.any(Number), + queryCacheMaxValueBytes, + ); }); - test("scopes signed-in cache entries by user and rejects oversized values", async () => { + test("rejects oversized values", async () => { const db = createDb(); const session = createSession("user-1"); await insertUserAsync(db, session.user.id); @@ -161,34 +140,9 @@ describe("queryCacheRouter", () => { const caller = queryCacheRouter.createCaller({ db, deviceType: undefined, session }); const value = "x".repeat(queryCacheMaxValueBytes + 1); - await expect(caller.setItem({ boardId: "board-1", key: "query-key", value })).resolves.toEqual({ + await expect(caller.setItem({ boardId: "board-1", value })).resolves.toEqual({ stored: false, }); - expect(redisMock.calls.at(-1)).toMatchObject({ - userId: "user-1", - boardId: "board-1", - ttlMs: queryCacheRetentionMs, - maxValueBytes: queryCacheMaxValueBytes, - }); - }); - - test("expires values through the Redis TTL path", async () => { - vi.useFakeTimers(); - vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z")); - - const db = createDb(); - const session = createSession("user-1"); - await insertUserAsync(db, session.user.id); - await db.insert(boards).values({ id: "board-1", name: "board", creatorId: session.user.id, isPublic: false }); - - const caller = queryCacheRouter.createCaller({ db, deviceType: undefined, session }); - - await caller.setItem({ boardId: "board-1", key: "query-key", value: "cached-value" }); - await expect(caller.getItem({ boardId: "board-1", key: "query-key" })).resolves.toBe("cached-value"); - - await vi.advanceTimersByTimeAsync(queryCacheRetentionMs + 1); - - await expect(caller.getItem({ boardId: "board-1", key: "query-key" })).resolves.toBeNull(); }); }); diff --git a/packages/api/src/trpc.ts b/packages/api/src/trpc.ts index 0ef944669..6deb06b22 100644 --- a/packages/api/src/trpc.ts +++ b/packages/api/src/trpc.ts @@ -95,8 +95,12 @@ export const createTRPCRouter = t.router; export const isDemoMode = env.DEMO_MODE; +// The public "real demo" (demo + mock integrations) stays read-only, while preview +// deployments run with DEMO_MODE + DEMO_READ_ONLY=false to allow mutations. +export const isDemoReadOnly = env.DEMO_MODE && env.DEMO_READ_ONLY; + const enforceDemoModeReadOnly = t.middleware(({ ctx, next, type }) => { - if (env.DEMO_MODE && type === "mutation") { + if (isDemoReadOnly && type === "mutation") { throw new TRPCError({ code: "FORBIDDEN", message: "Mutations are disabled in demo mode", @@ -105,7 +109,7 @@ const enforceDemoModeReadOnly = t.middleware(({ ctx, next, type }) => { return next({ ctx }); }); -const baseProcedure = env.DEMO_MODE ? t.procedure.use(enforceDemoModeReadOnly) : t.procedure; +const baseProcedure = isDemoReadOnly ? t.procedure.use(enforceDemoModeReadOnly) : t.procedure; /** * Public (unauthed) procedure @@ -116,6 +120,8 @@ const baseProcedure = env.DEMO_MODE ? t.procedure.use(enforceDemoModeReadOnly) : */ export const publicProcedure = baseProcedure; +export const internalProcedure = t.procedure; + const enforceUserIsAuthed = t.middleware(({ ctx, next }) => { if (!ctx.session?.user) { throw new TRPCError({ code: "UNAUTHORIZED" }); diff --git a/packages/auth/providers/credentials/authorization/ldap-authorization.ts b/packages/auth/providers/credentials/authorization/ldap-authorization.ts index 592fde682..31fa7593d 100644 --- a/packages/auth/providers/credentials/authorization/ldap-authorization.ts +++ b/packages/auth/providers/credentials/authorization/ldap-authorization.ts @@ -1,5 +1,5 @@ import { CredentialsSignin } from "@auth/core/errors"; -import { z } from "zod/v4"; +import type { z } from "zod/v4"; import { createId } from "@homarr/common"; import { createLogger } from "@homarr/core/infrastructure/logs"; @@ -7,6 +7,7 @@ import type { Database, InferInsertModel } from "@homarr/db"; import { and, eq } from "@homarr/db"; import { users } from "@homarr/db/schema"; import type { ldapSignInSchema } from "@homarr/validation/user"; +import { utf8EmailSchema } from "@homarr/validation/email"; import { env } from "../../../env"; import { LdapClient } from "../ldap-client"; @@ -52,8 +53,7 @@ export const authorizeWithLdapCredentialsAsync = async ( throw new CredentialsSignin(`User not found in LDAP username="${credentials.name}"`); } - // Validate email - const mailResult = await z.string().email().safeParseAsync(ldapUser[env.AUTH_LDAP_USER_MAIL_ATTRIBUTE]); + const mailResult = await utf8EmailSchema.safeParseAsync(ldapUser[env.AUTH_LDAP_USER_MAIL_ATTRIBUTE]); if (!mailResult.success) { logger.error("User found in LDAP but with invalid or non-existing Email", { diff --git a/packages/boards/src/context.tsx b/packages/boards/src/context.tsx index c2ccc690a..b482fbe0c 100644 --- a/packages/boards/src/context.tsx +++ b/packages/boards/src/context.tsx @@ -20,16 +20,7 @@ export const BoardProvider = ({ }: PropsWithChildren<{ initialBoard: RouterOutputs["board"]["getBoardByName"]; }>) => { - const { data } = clientApi.board.getBoardByName.useQuery( - { name: initialBoard.name }, - { - initialData: initialBoard, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - }, - ); - - setActiveQueryCacheBoardId(data.id); + const { data } = clientApi.board.getBoardByName.useQuery({ name: initialBoard.name }, { initialData: initialBoard }); // Update the board name so it can be used within updateBoard method updateBoardName(initialBoard.name); diff --git a/packages/common/src/cookie.ts b/packages/common/src/cookie.ts index 73dfb0c89..e519e2ff3 100644 --- a/packages/common/src/cookie.ts +++ b/packages/common/src/cookie.ts @@ -6,5 +6,5 @@ export function parseCookies(cookieString: string) { } export function setClientCookie(name: string, value: string, options: SerializeOptions = {}) { - document.cookie = stringifySetCookie(name, value, options); + document.cookie = stringifySetCookie({ name, value, ...options }); } diff --git a/packages/common/src/date.ts b/packages/common/src/date.ts index de9ecadaf..5d46e45e1 100644 --- a/packages/common/src/date.ts +++ b/packages/common/src/date.ts @@ -17,6 +17,12 @@ export function formatDuration(milliseconds: number) { return `${minutes}:${String(seconds).padStart(2, "0")}`; } +export const toValidDate = (value: unknown): Date | null => { + if (value == null || typeof value === "boolean") return null; + const date = value instanceof Date ? value : new Date(value as string | number); + return Number.isFinite(date.getTime()) ? date : null; +}; + export const isDateWithin = (date: Date, relativeDate: string): boolean => { if (relativeDate.length < 2) { throw new Error("Relative date must be at least 2 characters long"); diff --git a/packages/common/src/errors/http/handlers/axios-http-error-handler.ts b/packages/common/src/errors/http/handlers/axios-http-error-handler.ts index 200d2cd0b..cb454dcde 100644 --- a/packages/common/src/errors/http/handlers/axios-http-error-handler.ts +++ b/packages/common/src/errors/http/handlers/axios-http-error-handler.ts @@ -25,7 +25,7 @@ export class AxiosHttpErrorHandler extends HttpErrorHandler { return new RequestError(requestErrorInput, { cause: error, - }); + }) as AnyRequestError; } handleResponseError(error: unknown): ResponseError | undefined { if (!(error instanceof AxiosError)) return undefined; diff --git a/packages/common/src/errors/http/handlers/fetch-http-error-handler.ts b/packages/common/src/errors/http/handlers/fetch-http-error-handler.ts index f9e3fb1cc..39b51b3db 100644 --- a/packages/common/src/errors/http/handlers/fetch-http-error-handler.ts +++ b/packages/common/src/errors/http/handlers/fetch-http-error-handler.ts @@ -20,7 +20,7 @@ export class FetchHttpErrorHandler extends HttpErrorHandler { const result = matchErrorCode(error.cause.code); if (!result) return undefined; - return new RequestError(result, { cause: error }); + return new RequestError(result, { cause: error }) as AnyRequestError; } /** @@ -59,7 +59,7 @@ export const matchErrorCode = (code: string): AnyRequestErrorInput | undefined = type: key, reason: found[0] as RequestErrorReason, code: code as RequestErrorCode, - }; + } as AnyRequestErrorInput; } return undefined; diff --git a/packages/common/src/errors/http/handlers/node-fetch-http-error-handler.ts b/packages/common/src/errors/http/handlers/node-fetch-http-error-handler.ts index 4f70395de..78b6a69c3 100644 --- a/packages/common/src/errors/http/handlers/node-fetch-http-error-handler.ts +++ b/packages/common/src/errors/http/handlers/node-fetch-http-error-handler.ts @@ -30,7 +30,7 @@ export class NodeFetchHttpErrorHandler extends HttpErrorHandler { return new RequestError(requestErrorInput, { cause: error, - }); + }) as AnyRequestError; } /** diff --git a/packages/core/src/infrastructure/http/request.ts b/packages/core/src/infrastructure/http/request.ts index 9bfc376fa..b13eb0cc2 100644 --- a/packages/core/src/infrastructure/http/request.ts +++ b/packages/core/src/infrastructure/http/request.ts @@ -2,7 +2,7 @@ import type { AgentOptions } from "node:https"; import { Agent as HttpsAgent } from "node:https"; import { checkServerIdentity } from "node:tls"; import axios from "axios"; -import type { RequestInfo, RequestInit, Response } from "undici"; +import type { Agent as UndiciAgent, RequestInfo, RequestInit, Response } from "undici"; import { fetch } from "undici"; import { @@ -29,11 +29,15 @@ export const createCustomCheckServerIdentity = ( }; }; -export const createCertificateAgentAsync = async (override?: { - ca: string | string[]; - checkServerIdentity: typeof checkServerIdentity; -}) => { +export const createCertificateAgentAsync = async ( + override?: { + ca: string | string[]; + checkServerIdentity: typeof checkServerIdentity; + }, + agentOptions?: Pick, +) => { return new UndiciHttpAgent({ + ...agentOptions, connect: override ?? { ca: await getAllTrustedCertificatesAsync(), checkServerIdentity: createCustomCheckServerIdentity(await getTrustedCertificateHostnamesAsync()), @@ -61,14 +65,18 @@ export const createAxiosCertificateInstanceAsync = async ( export const fetchWithTrustedCertificatesAsync = async ( url: RequestInfo, - options?: RequestInit & { timeout?: number }, + options?: RequestInit & { timeout?: number; bodyTimeout?: number }, ): Promise => { - const agent = await createCertificateAgentAsync(undefined); + const agent = await createCertificateAgentAsync( + undefined, + options?.bodyTimeout !== undefined ? { bodyTimeout: options.bodyTimeout } : undefined, + ); if (options?.timeout) { + const { bodyTimeout: _bodyTimeout, ...fetchOptions } = options; return await withTimeoutAsync( async (signal) => fetch(url, { - ...options, + ...fetchOptions, signal, dispatcher: agent, }), @@ -76,8 +84,9 @@ export const fetchWithTrustedCertificatesAsync = async ( ); } + const { bodyTimeout: _bodyTimeout, ...fetchOptions } = options ?? {}; return fetch(url, { - ...options, + ...fetchOptions, dispatcher: agent, }); }; diff --git a/packages/cron-jobs/src/index.ts b/packages/cron-jobs/src/index.ts index 1f0cb8577..d9e42493e 100644 --- a/packages/cron-jobs/src/index.ts +++ b/packages/cron-jobs/src/index.ts @@ -1,32 +1,6 @@ import { analyticsJob } from "./jobs/analytics"; -import { dockerContainersJob } from "./jobs/docker"; import { iconsUpdaterJob } from "./jobs/icons-updater"; -import { dnsHoleJob } from "./jobs/integrations/dns-hole"; -import { downloadsJob } from "./jobs/integrations/downloads"; -import { - firewallCpuJob, - firewallInterfacesJob, - firewallMemoryJob, - firewallVersionJob, -} from "./jobs/integrations/firewall"; -import { healthMonitoringJob } from "./jobs/integrations/health-monitoring"; -import { smartHomeEntityStateJob } from "./jobs/integrations/home-assistant"; -import { indexerManagerJob } from "./jobs/integrations/indexer-manager"; -import { mediaOrganizerJob } from "./jobs/integrations/media-organizer"; -import { mediaRequestListJob, mediaRequestStatsJob } from "./jobs/integrations/media-requests"; -import { mediaServerJob } from "./jobs/integrations/media-server"; -import { mediaTranscodingJob } from "./jobs/integrations/media-transcoding"; -import { networkControllerJob } from "./jobs/integrations/network-controller"; -import { refreshNotificationsJob } from "./jobs/integrations/notifications"; -import { timetableJob } from "./jobs/integrations/timetable"; -import { tracearrJob } from "./jobs/integrations/tracearr"; -import { umamiActiveVisitorsJob } from "./jobs/integrations/umami"; -import { vpnJob } from "./jobs/integrations/vpn"; -import { minecraftServerStatusJob } from "./jobs/minecraft-server-status"; import { pingJob } from "./jobs/ping"; -import { rssFeedsJob } from "./jobs/rss-feeds"; -import { updateCheckerJob } from "./jobs/update-checker"; -import { weatherJob } from "./jobs/weather"; import { createCronJobGroup } from "./lib"; const getJobGroup = () => { @@ -34,35 +8,9 @@ const getJobGroup = () => { analytics: analyticsJob, iconsUpdater: iconsUpdaterJob, ping: pingJob, - smartHomeEntityState: smartHomeEntityStateJob, - mediaServer: mediaServerJob, - mediaOrganizer: mediaOrganizerJob, - downloads: downloadsJob, - dnsHole: dnsHoleJob, - mediaRequestStats: mediaRequestStatsJob, - mediaRequestList: mediaRequestListJob, - rssFeeds: rssFeedsJob, - indexerManager: indexerManagerJob, - healthMonitoring: healthMonitoringJob, - updateChecker: updateCheckerJob, - mediaTranscoding: mediaTranscodingJob, - minecraftServerStatus: minecraftServerStatusJob, - dockerContainers: dockerContainersJob, - networkController: networkControllerJob, - firewallCpu: firewallCpuJob, - firewallMemory: firewallMemoryJob, - firewallVersion: firewallVersionJob, - firewallInterfaces: firewallInterfacesJob, - refreshNotifications: refreshNotificationsJob, - weather: weatherJob, - timetable: timetableJob, - tracearr: tracearrJob, - umamiActiveVisitors: umamiActiveVisitorsJob, - vpn: vpnJob, }); }; -// Add global type augmentation for homarr declare global { var cronJobs: ReturnType | undefined; } diff --git a/packages/cron-jobs/src/jobs/docker.ts b/packages/cron-jobs/src/jobs/docker.ts deleted file mode 100644 index 963f395bf..000000000 --- a/packages/cron-jobs/src/jobs/docker.ts +++ /dev/null @@ -1,31 +0,0 @@ -import SuperJSON from "superjson"; - -import { createLogger } from "@homarr/core/infrastructure/logs"; -import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; -import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; -import { db, eq } from "@homarr/db"; -import { items } from "@homarr/db/schema"; -import { dockerContainersRequestHandler } from "@homarr/request-handler/docker"; - -import type { WidgetComponentProps } from "../../../widgets"; -import { createCronJob } from "../lib"; - -const logger = createLogger({ module: "dockerJobs" }); - -export const dockerContainersJob = createCronJob("dockerContainers", EVERY_MINUTE).withCallback(async () => { - const dockerItems = await db.query.items.findMany({ - where: eq(items.kind, "dockerContainers"), - }); - - await Promise.allSettled( - dockerItems.map(async (item) => { - try { - const options = SuperJSON.parse["options"]>(item.options); - const innerHandler = dockerContainersRequestHandler.handler(options); - await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true }); - } catch (error) { - logger.error(new ErrorWithMetadata("Failed to update Docker container status", { item }, { cause: error })); - } - }), - ); -}); diff --git a/packages/cron-jobs/src/jobs/integrations/dns-hole.ts b/packages/cron-jobs/src/jobs/integrations/dns-hole.ts deleted file mode 100644 index a78b28e08..000000000 --- a/packages/cron-jobs/src/jobs/integrations/dns-hole.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; -import { dnsHoleRequestHandler } from "@homarr/request-handler/dns-hole"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; - -import { createCronJob } from "../../lib"; - -export const dnsHoleJob = createCronJob("dnsHole", EVERY_MINUTE).withCallback( - createRequestIntegrationJobHandler(dnsHoleRequestHandler.handler, { - widgetKinds: ["dnsHoleSummary", "dnsHoleControls"], - getInput: { - dnsHoleSummary: () => ({}), - dnsHoleControls: () => ({}), - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/integrations/downloads.ts b/packages/cron-jobs/src/jobs/integrations/downloads.ts deleted file mode 100644 index 54eb476dc..000000000 --- a/packages/cron-jobs/src/jobs/integrations/downloads.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { EVERY_5_SECONDS } from "@homarr/cron-jobs-core/expressions"; -import { downloadClientRequestHandler } from "@homarr/request-handler/downloads"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; - -import { createCronJob } from "../../lib"; - -export const downloadsJob = createCronJob("downloads", EVERY_5_SECONDS).withCallback( - createRequestIntegrationJobHandler(downloadClientRequestHandler.handler, { - widgetKinds: ["downloads"], - getInput: { - downloads: (options) => ({ - limit: options.limitPerIntegration, - }), - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/integrations/firewall.ts b/packages/cron-jobs/src/jobs/integrations/firewall.ts deleted file mode 100644 index 50637a642..000000000 --- a/packages/cron-jobs/src/jobs/integrations/firewall.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { EVERY_5_SECONDS, EVERY_30_SECONDS, EVERY_HOUR, EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; -import { - firewallCpuRequestHandler, - firewallInterfacesRequestHandler, - firewallMemoryRequestHandler, - firewallVersionRequestHandler, -} from "@homarr/request-handler/firewall"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; - -import { createCronJob } from "../../lib"; - -export const firewallCpuJob = createCronJob("firewallCpu", EVERY_5_SECONDS).withCallback( - createRequestIntegrationJobHandler(firewallCpuRequestHandler.handler, { - widgetKinds: ["firewall"], - getInput: { - firewall: () => ({}), - }, - }), -); - -export const firewallMemoryJob = createCronJob("firewallMemory", EVERY_MINUTE).withCallback( - createRequestIntegrationJobHandler(firewallMemoryRequestHandler.handler, { - widgetKinds: ["firewall"], - getInput: { - firewall: () => ({}), - }, - }), -); - -export const firewallInterfacesJob = createCronJob("firewallInterfaces", EVERY_30_SECONDS).withCallback( - createRequestIntegrationJobHandler(firewallInterfacesRequestHandler.handler, { - widgetKinds: ["firewall"], - getInput: { - firewall: () => ({}), - }, - }), -); - -export const firewallVersionJob = createCronJob("firewallVersion", EVERY_HOUR).withCallback( - createRequestIntegrationJobHandler(firewallVersionRequestHandler.handler, { - widgetKinds: ["firewall"], - getInput: { - firewall: () => ({}), - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/integrations/health-monitoring.ts b/packages/cron-jobs/src/jobs/integrations/health-monitoring.ts deleted file mode 100644 index 4d09801d2..000000000 --- a/packages/cron-jobs/src/jobs/integrations/health-monitoring.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { EVERY_5_SECONDS } from "@homarr/cron-jobs-core/expressions"; -import { coolifyRequestHandler } from "@homarr/request-handler/coolify"; -import { clusterInfoRequestHandler, systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; - -import { createCronJob } from "../../lib"; - -export const healthMonitoringJob = createCronJob("healthMonitoring", EVERY_5_SECONDS).withCallback( - createRequestIntegrationJobHandler( - (integration, itemOptions: Record) => { - const { kind } = integration; - - if (kind === "beszel") { - return null as never; - } - if (kind !== "proxmox" && kind !== "mock" && kind !== "coolify") { - return systemInfoRequestHandler.handler({ ...integration, kind }, itemOptions); - } - if (kind === "coolify") { - return coolifyRequestHandler.handler({ ...integration, kind }, itemOptions); - } - return clusterInfoRequestHandler.handler({ ...integration, kind }, itemOptions); - }, - { - widgetKinds: ["healthMonitoring", "systemResources", "coolify"], - getInput: { - healthMonitoring: () => ({}), - systemResources: () => ({}), - coolify: () => ({}), - }, - }, - ), -); diff --git a/packages/cron-jobs/src/jobs/integrations/home-assistant.ts b/packages/cron-jobs/src/jobs/integrations/home-assistant.ts deleted file mode 100644 index 2723391a7..000000000 --- a/packages/cron-jobs/src/jobs/integrations/home-assistant.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; -import { smartHomeEntityStateRequestHandler } from "@homarr/request-handler/smart-home-entity-state"; - -import { createCronJob } from "../../lib"; - -export const smartHomeEntityStateJob = createCronJob("smartHomeEntityState", EVERY_MINUTE).withCallback( - createRequestIntegrationJobHandler(smartHomeEntityStateRequestHandler.handler, { - widgetKinds: ["smartHome-entityState"], - getInput: { - "smartHome-entityState": (options) => ({ - entityId: options.entityId, - }), - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/integrations/indexer-manager.ts b/packages/cron-jobs/src/jobs/integrations/indexer-manager.ts deleted file mode 100644 index 11da75508..000000000 --- a/packages/cron-jobs/src/jobs/integrations/indexer-manager.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions"; -import { indexerManagerRequestHandler } from "@homarr/request-handler/indexer-manager"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; - -import { createCronJob } from "../../lib"; - -export const indexerManagerJob = createCronJob("indexerManager", EVERY_5_MINUTES).withCallback( - createRequestIntegrationJobHandler(indexerManagerRequestHandler.handler, { - widgetKinds: ["indexerManager"], - getInput: { - indexerManager: () => ({}), - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/integrations/media-organizer.ts b/packages/cron-jobs/src/jobs/integrations/media-organizer.ts deleted file mode 100644 index a84696da9..000000000 --- a/packages/cron-jobs/src/jobs/integrations/media-organizer.ts +++ /dev/null @@ -1,36 +0,0 @@ -import dayjs from "dayjs"; - -import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; -import { calendarMonthRequestHandler } from "@homarr/request-handler/calendar"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; - -import { createCronJob } from "../../lib"; - -export const mediaOrganizerJob = createCronJob("mediaOrganizer", EVERY_MINUTE).withCallback( - createRequestIntegrationJobHandler(calendarMonthRequestHandler.handler, { - widgetKinds: ["calendar"], - getInput: { - // Request handler will run for all specified months - calendar: (options) => { - const inputs = []; - - const startOffset = -Number(options.filterPastMonths); - const endOffset = Number(options.filterFutureMonths); - - for (let offsetMonths = startOffset; offsetMonths <= endOffset; offsetMonths++) { - const year = dayjs().subtract(offsetMonths, "months").year(); - const month = dayjs().subtract(offsetMonths, "months").month(); - - inputs.push({ - year, - month, - releaseType: options.releaseType, - showUnmonitored: options.showUnmonitored, - }); - } - - return inputs; - }, - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/integrations/media-requests.ts b/packages/cron-jobs/src/jobs/integrations/media-requests.ts deleted file mode 100644 index ededb6141..000000000 --- a/packages/cron-jobs/src/jobs/integrations/media-requests.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; -import { mediaRequestListRequestHandler } from "@homarr/request-handler/media-request-list"; -import { mediaRequestStatsRequestHandler } from "@homarr/request-handler/media-request-stats"; - -import { createCronJob } from "../../lib"; - -export const mediaRequestStatsJob = createCronJob("mediaRequestStats", EVERY_MINUTE).withCallback( - createRequestIntegrationJobHandler(mediaRequestStatsRequestHandler.handler, { - widgetKinds: ["mediaRequests-requestStats"], - getInput: { - "mediaRequests-requestStats": () => ({}), - }, - }), -); - -export const mediaRequestListJob = createCronJob("mediaRequestList", EVERY_MINUTE).withCallback( - createRequestIntegrationJobHandler(mediaRequestListRequestHandler.handler, { - widgetKinds: ["mediaRequests-requestList"], - getInput: { - "mediaRequests-requestList": () => ({}), - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/integrations/media-server.ts b/packages/cron-jobs/src/jobs/integrations/media-server.ts deleted file mode 100644 index 099ea53ae..000000000 --- a/packages/cron-jobs/src/jobs/integrations/media-server.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { EVERY_5_SECONDS } from "@homarr/cron-jobs-core/expressions"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; -import { mediaServerRequestHandler } from "@homarr/request-handler/media-server"; - -import { createCronJob } from "../../lib"; - -export const mediaServerJob = createCronJob("mediaServer", EVERY_5_SECONDS).withCallback( - createRequestIntegrationJobHandler(mediaServerRequestHandler.handler, { - widgetKinds: ["mediaServer"], - getInput: { - mediaServer: ({ showOnlyPlaying }) => ({ showOnlyPlaying }), - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/integrations/media-transcoding.ts b/packages/cron-jobs/src/jobs/integrations/media-transcoding.ts deleted file mode 100644 index c039dc250..000000000 --- a/packages/cron-jobs/src/jobs/integrations/media-transcoding.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; -import { mediaTranscodingRequestHandler } from "@homarr/request-handler/media-transcoding"; - -import { createCronJob } from "../../lib"; - -export const mediaTranscodingJob = createCronJob("mediaTranscoding", EVERY_5_MINUTES).withCallback( - createRequestIntegrationJobHandler(mediaTranscodingRequestHandler.handler, { - widgetKinds: ["mediaTranscoding"], - getInput: { - mediaTranscoding: () => ({ pageOffset: 0, pageSize: 10 }), - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/integrations/network-controller.ts b/packages/cron-jobs/src/jobs/integrations/network-controller.ts deleted file mode 100644 index 5d9354588..000000000 --- a/packages/cron-jobs/src/jobs/integrations/network-controller.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; -import { networkControllerRequestHandler } from "@homarr/request-handler/network-controller"; - -import { createCronJob } from "../../lib"; - -export const networkControllerJob = createCronJob("networkController", EVERY_MINUTE).withCallback( - createRequestIntegrationJobHandler(networkControllerRequestHandler.handler, { - widgetKinds: ["networkControllerSummary"], - getInput: { - networkControllerSummary: () => ({}), - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/integrations/notifications.ts b/packages/cron-jobs/src/jobs/integrations/notifications.ts deleted file mode 100644 index 0f544995f..000000000 --- a/packages/cron-jobs/src/jobs/integrations/notifications.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; -import { notificationsRequestHandler } from "@homarr/request-handler/notifications"; - -import { createCronJob } from "../../lib"; - -export const refreshNotificationsJob = createCronJob("refreshNotifications", EVERY_5_MINUTES).withCallback( - createRequestIntegrationJobHandler(notificationsRequestHandler.handler, { - widgetKinds: ["notifications"], - getInput: { - notifications: (options) => options, - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/integrations/timetable.ts b/packages/cron-jobs/src/jobs/integrations/timetable.ts deleted file mode 100644 index b3964dc38..000000000 --- a/packages/cron-jobs/src/jobs/integrations/timetable.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; -import { timetableGetTimetableRequestHandler } from "@homarr/request-handler/timetable"; - -import { createCronJob } from "../../lib"; - -export const timetableJob = createCronJob("timetable", EVERY_5_MINUTES).withCallback( - createRequestIntegrationJobHandler(timetableGetTimetableRequestHandler.handler, { - widgetKinds: ["timetable"], - getInput: { - timetable: (options) => - options.station - ? { - stationId: options.station.value, - limit: 10, - } - : [], - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/integrations/tracearr.ts b/packages/cron-jobs/src/jobs/integrations/tracearr.ts deleted file mode 100644 index f80130374..000000000 --- a/packages/cron-jobs/src/jobs/integrations/tracearr.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { EVERY_5_SECONDS } from "@homarr/cron-jobs-core/expressions"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; -import { tracearrRequestHandler } from "@homarr/request-handler/tracearr"; - -import { createCronJob } from "../../lib"; - -export const tracearrJob = createCronJob("tracearr", EVERY_5_SECONDS).withCallback( - createRequestIntegrationJobHandler(tracearrRequestHandler.handler, { - widgetKinds: ["tracearr"], - getInput: { - tracearr: () => ({}), - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/integrations/umami.ts b/packages/cron-jobs/src/jobs/integrations/umami.ts deleted file mode 100644 index c50a3c604..000000000 --- a/packages/cron-jobs/src/jobs/integrations/umami.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { EVERY_30_SECONDS } from "@homarr/cron-jobs-core/expressions"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; -import { umamiActiveVisitorsRequestHandler } from "@homarr/request-handler/umami"; - -import { createCronJob } from "../../lib"; - -export const umamiActiveVisitorsJob = createCronJob("umamiActiveVisitors", EVERY_30_SECONDS).withCallback( - createRequestIntegrationJobHandler(umamiActiveVisitorsRequestHandler.handler, { - widgetKinds: ["umami"], - getInput: { - umami: (options) => (options.websiteId ? { websiteId: options.websiteId } : []), - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/integrations/vpn.ts b/packages/cron-jobs/src/jobs/integrations/vpn.ts deleted file mode 100644 index b6495b359..000000000 --- a/packages/cron-jobs/src/jobs/integrations/vpn.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { EVERY_30_SECONDS } from "@homarr/cron-jobs-core/expressions"; -import { vpnSummaryHandler } from "@homarr/request-handler/vpn"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; - -import { createCronJob } from "../../lib"; - -export const vpnJob = createCronJob("vpn", EVERY_30_SECONDS).withCallback( - createRequestIntegrationJobHandler(vpnSummaryHandler.handler, { - widgetKinds: ["vpn"], - getInput: { - vpn: () => ({}), - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/minecraft-server-status.ts b/packages/cron-jobs/src/jobs/minecraft-server-status.ts deleted file mode 100644 index e6c084702..000000000 --- a/packages/cron-jobs/src/jobs/minecraft-server-status.ts +++ /dev/null @@ -1,25 +0,0 @@ -import SuperJSON from "superjson"; - -import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions"; -import { db, eq } from "@homarr/db"; -import { items } from "@homarr/db/schema"; -import { minecraftServerStatusRequestHandler } from "@homarr/request-handler/minecraft-server-status"; - -import type { WidgetComponentProps } from "../../../widgets/src"; -import { createCronJob } from "../lib"; - -export const minecraftServerStatusJob = createCronJob("minecraftServerStatus", EVERY_5_MINUTES).withCallback( - async () => { - const dbItems = await db.query.items.findMany({ - where: eq(items.kind, "minecraftServerStatus"), - }); - - await Promise.allSettled( - dbItems.map(async (item) => { - const options = SuperJSON.parse["options"]>(item.options); - const innerHandler = minecraftServerStatusRequestHandler.handler(options); - await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true }); - }), - ); - }, -); diff --git a/packages/cron-jobs/src/jobs/rss-feeds.ts b/packages/cron-jobs/src/jobs/rss-feeds.ts deleted file mode 100644 index 6a497a043..000000000 --- a/packages/cron-jobs/src/jobs/rss-feeds.ts +++ /dev/null @@ -1,39 +0,0 @@ -import SuperJSON from "superjson"; - -import { createLogger } from "@homarr/core/infrastructure/logs"; -import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; -import { EVERY_10_MINUTES } from "@homarr/cron-jobs-core/expressions"; -import { db, eq } from "@homarr/db"; -import { items } from "@homarr/db/schema"; -import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds"; - -// This import is done that way to avoid circular dependencies. -import type { WidgetComponentProps } from "../../../widgets"; -import { createCronJob } from "../lib"; - -const logger = createLogger({ module: "rssFeedsJobs" }); - -export const rssFeedsJob = createCronJob("rssFeeds", EVERY_10_MINUTES).withCallback(async () => { - const rssItems = await db.query.items.findMany({ - where: eq(items.kind, "rssFeed"), - }); - - const itemOptions = rssItems.map((item) => SuperJSON.parse["options"]>(item.options)); - - for (const option of itemOptions) { - const maxAmountPosts = typeof option.maximumAmountPosts === "number" ? option.maximumAmountPosts : 100; - for (const url of option.feedUrls) { - try { - const innerHandler = rssFeedsRequestHandler.handler({ - url, - count: maxAmountPosts, - }); - await innerHandler.getCachedOrUpdatedDataAsync({ - forceUpdate: true, - }); - } catch (error) { - logger.error(new ErrorWithMetadata("Failed to update RSS feed", { url }, { cause: error })); - } - } - } -}); diff --git a/packages/cron-jobs/src/jobs/update-checker.ts b/packages/cron-jobs/src/jobs/update-checker.ts deleted file mode 100644 index eea68d07b..000000000 --- a/packages/cron-jobs/src/jobs/update-checker.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { EVERY_DAY } from "@homarr/cron-jobs-core/expressions"; -import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker"; - -import { createCronJob } from "../lib"; - -export const updateCheckerJob = createCronJob("updateChecker", EVERY_DAY, { - runOnStart: true, -}).withCallback(async () => { - const handler = updateCheckerRequestHandler.handler({}); - await handler.getCachedOrUpdatedDataAsync({}); -}); diff --git a/packages/cron-jobs/src/jobs/weather.ts b/packages/cron-jobs/src/jobs/weather.ts deleted file mode 100644 index 1b23ade4c..000000000 --- a/packages/cron-jobs/src/jobs/weather.ts +++ /dev/null @@ -1,36 +0,0 @@ -import SuperJSON from "superjson"; - -import { createLogger } from "@homarr/core/infrastructure/logs"; -import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; -import { EVERY_10_MINUTES } from "@homarr/cron-jobs-core/expressions"; -import { db, eq } from "@homarr/db"; -import { items } from "@homarr/db/schema"; -import { weatherRequestHandler } from "@homarr/request-handler/weather"; - -import type { WidgetComponentProps } from "../../../widgets"; -import { createCronJob } from "../lib"; - -const logger = createLogger({ module: "weatherJobs" }); - -export const weatherJob = createCronJob("weather", EVERY_10_MINUTES).withCallback(async () => { - const weatherItems = await db.query.items.findMany({ - where: eq(items.kind, "weather"), - }); - - const parsedItems = weatherItems.map((item) => ({ - id: item.id, - options: SuperJSON.parse["options"]>(item.options), - })); - - for (const item of parsedItems) { - try { - const innerHandler = weatherRequestHandler.handler({ - longitude: item.options.location.longitude, - latitude: item.options.location.latitude, - }); - await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true }); - } catch (error) { - logger.error(new ErrorWithMetadata("Failed to update weather", { id: item.id }, { cause: error })); - } - } -}); diff --git a/packages/db/migrations/custom/0000_release_widget_provider_to_options.ts b/packages/db/migrations/custom/0000_release_widget_provider_to_options.ts index 951831482..85f39bcbc 100644 --- a/packages/db/migrations/custom/0000_release_widget_provider_to_options.ts +++ b/packages/db/migrations/custom/0000_release_widget_provider_to_options.ts @@ -1,8 +1,7 @@ import SuperJSON from "superjson"; import { createId } from "@homarr/common"; -import type { IntegrationKind } from "@homarr/definitions"; -import { getIntegrationKindsByCategory } from "@homarr/definitions"; +import { releaseProviderKinds } from "@homarr/definitions"; import { eq } from "../.."; import type { Database } from "../.."; @@ -13,19 +12,6 @@ export async function migrateReleaseWidgetProviderToOptionsAsync(db: Database) { where: (items, { eq }) => eq(items.kind, "releases"), }); - const integrationKinds = getIntegrationKindsByCategory("releasesProvider"); - const providerIntegrations = await db.query.integrations.findMany({ - where: (integrations, { inArray }) => inArray(integrations.kind, integrationKinds), - columns: { - id: true, - kind: true, - }, - }); - - const providerIntegrationMap = new Map( - providerIntegrations.map((integration) => [integration.kind, integration.id]), - ); - const updates: { id: string; options: object; @@ -37,18 +23,19 @@ export async function migrateReleaseWidgetProviderToOptionsAsync(db: Database) { if (options.repositories.length === 0) continue; if (!options.repositories.some((repository) => "providerKey" in repository)) continue; - const updatedRepositories = options.repositories.map( - ({ providerKey, ...otherFields }: { providerKey: string; [key: string]: unknown }) => { - // Ensure providerKey is camelCase + const updatedRepositories = options.repositories + .map(({ providerKey, ...otherFields }: { providerKey: string; [key: string]: unknown }) => { const provider = providerKey.charAt(0).toLowerCase() + providerKey.slice(1); + const matchedProvider = releaseProviderKinds.find((kind) => kind === provider); + if (!matchedProvider) return null; return { id: createId(), - providerIntegrationId: providerIntegrationMap.get(provider as IntegrationKind) ?? null, + provider: matchedProvider, ...otherFields, }; - }, - ); + }) + .filter((repo) => repo !== null); updates.push({ id: item.id, @@ -68,5 +55,5 @@ export async function migrateReleaseWidgetProviderToOptionsAsync(db: Database) { .where(eq(items.id, update.id)); } - console.log(`Migrated release widget providers to integrations count="${updates.length}"`); + console.log(`Migrated release widget providers to options count="${updates.length}"`); } diff --git a/packages/db/migrations/custom/0003_remove_widget_only_integrations.ts b/packages/db/migrations/custom/0003_remove_widget_only_integrations.ts new file mode 100644 index 000000000..0c7a04d6c --- /dev/null +++ b/packages/db/migrations/custom/0003_remove_widget_only_integrations.ts @@ -0,0 +1,100 @@ +import SuperJSON from "superjson"; + +import { eq, inArray } from "../.."; +import type { Database } from "../.."; +import { integrationItems, integrations, items } from "../../schema"; + +const removedIntegrationKinds = [ + "github", + "dockerHub", + "gitlab", + "npm", + "codeberg", + "linuxServerIO", + "gitHubContainerRegistry", + "quay", + "searchCh", +] as const; + +type RemovedIntegrationKind = (typeof removedIntegrationKinds)[number]; + +const releaseProviderKinds = [ + "github", + "dockerHub", + "gitlab", + "npm", + "codeberg", + "linuxServerIO", + "gitHubContainerRegistry", + "quay", +] as const; + +const isReleaseProviderKind = (kind: RemovedIntegrationKind): kind is (typeof releaseProviderKinds)[number] => + releaseProviderKinds.some((provider) => provider === kind); + +export async function migrateWidgetOnlyIntegrationsToOptionsAsync(db: Database) { + const oldIntegrations = await db.query.integrations.findMany({ + columns: { id: true, kind: true, url: true }, + }); + const oldIntegrationById = new Map( + oldIntegrations + .filter((integration) => removedIntegrationKinds.includes(integration.kind as RemovedIntegrationKind)) + .map((integration) => [ + integration.id, + { kind: integration.kind as RemovedIntegrationKind, url: integration.url }, + ]), + ); + + if (oldIntegrationById.size === 0) return; + + const oldIntegrationIds = Array.from(oldIntegrationById.keys()); + const oldIntegrationItems = await db.query.integrationItems.findMany(); + const oldIntegrationItemByItemId = new Map( + oldIntegrationItems + .filter((item) => oldIntegrationById.has(item.integrationId)) + .map((item) => [item.itemId, oldIntegrationById.get(item.integrationId)]), + ); + const existingItems = await db.query.items.findMany({ + where: (items, { inArray }) => inArray(items.kind, ["releases", "timetable"]), + }); + + const updatedItems: { id: string; options: object }[] = []; + + for (const item of existingItems) { + const options = SuperJSON.parse>(item.options); + const linkedIntegration = oldIntegrationItemByItemId.get(item.id); + + if (item.kind === "releases" && Array.isArray(options.repositories)) { + const repositories = options.repositories.map((repository) => { + if (typeof repository !== "object" || repository === null) return repository; + const { providerIntegrationId, ...rest } = repository as Record; + const provider = + typeof providerIntegrationId === "string" ? oldIntegrationById.get(providerIntegrationId) : undefined; + const existingProviderUrl = + typeof rest.providerUrl === "string" && rest.providerUrl.length > 0 ? rest.providerUrl : undefined; + return { + ...rest, + provider: provider && isReleaseProviderKind(provider.kind) ? provider.kind : rest.provider, + providerUrl: provider?.url ?? existingProviderUrl, + }; + }); + updatedItems.push({ id: item.id, options: { ...options, repositories } }); + } + + if (item.kind === "timetable" && linkedIntegration?.kind === "searchCh") { + updatedItems.push({ id: item.id, options: { ...options, baseUrl: linkedIntegration.url } }); + } + } + + for (const update of updatedItems) { + await db + .update(items) + .set({ options: SuperJSON.stringify(update.options) }) + .where(eq(items.id, update.id)); + } + + await db.delete(integrationItems).where(inArray(integrationItems.integrationId, oldIntegrationIds)); + await db.delete(integrations).where(inArray(integrations.id, oldIntegrationIds)); + + console.log(`Migrated widget-only integrations to widget options count="${updatedItems.length}"`); +} diff --git a/packages/db/migrations/custom/index.ts b/packages/db/migrations/custom/index.ts index a6a002733..8bcc93c32 100644 --- a/packages/db/migrations/custom/index.ts +++ b/packages/db/migrations/custom/index.ts @@ -2,9 +2,11 @@ import type { Database } from "../.."; import { migrateReleaseWidgetProviderToOptionsAsync } from "./0000_release_widget_provider_to_options"; import { migrateOpnsenseCredentialsAsync } from "./0001_opnsense_credentials"; import { migrateAppWidgetShowDescriptionTooltipToDisplayModeAsync } from "./0002_app_widget_show_description_tooltip_to_display_mode"; +import { migrateWidgetOnlyIntegrationsToOptionsAsync } from "./0003_remove_widget_only_integrations"; export const applyCustomMigrationsAsync = async (db: Database) => { await migrateReleaseWidgetProviderToOptionsAsync(db); await migrateOpnsenseCredentialsAsync(db); await migrateAppWidgetShowDescriptionTooltipToDisplayModeAsync(db); + await migrateWidgetOnlyIntegrationsToOptionsAsync(db); }; diff --git a/packages/db/migrations/mysql/0042_add_widget_secret.sql b/packages/db/migrations/mysql/0042_add_widget_secret.sql new file mode 100644 index 000000000..4d150612d --- /dev/null +++ b/packages/db/migrations/mysql/0042_add_widget_secret.sql @@ -0,0 +1,9 @@ +CREATE TABLE `widget_secret` ( + `item_id` varchar(64) NOT NULL, + `kind` varchar(64) NOT NULL, + `value` text NOT NULL, + `updated_at` timestamp NOT NULL, + CONSTRAINT `widget_secret_item_id_kind_pk` PRIMARY KEY(`item_id`,`kind`) +); +--> statement-breakpoint +ALTER TABLE `widget_secret` ADD CONSTRAINT `widget_secret_item_id_item_id_fk` FOREIGN KEY (`item_id`) REFERENCES `item`(`id`) ON DELETE cascade ON UPDATE no action; diff --git a/packages/db/migrations/sqlite/meta/0041_snapshot.json b/packages/db/migrations/mysql/meta/0041_snapshot.json similarity index 83% rename from packages/db/migrations/sqlite/meta/0041_snapshot.json rename to packages/db/migrations/mysql/meta/0041_snapshot.json index 5ca97b3c6..1cbe65bde 100644 --- a/packages/db/migrations/sqlite/meta/0041_snapshot.json +++ b/packages/db/migrations/mysql/meta/0041_snapshot.json @@ -1,15 +1,15 @@ { - "version": "6", - "dialect": "sqlite", - "id": "f5f5085b-2601-4ea8-b0dd-a6705f1a136c", - "prevId": "b02e04f9-1462-4832-9a55-b26aded09a36", + "version": "5", + "dialect": "mysql", + "id": "8e2ec048-e89a-4d46-84ee-ae3da138b5ab", + "prevId": "5ed064c9-bc79-486d-b313-55c65bf2920a", "tables": { "account": { "name": "account", "columns": { "user_id": { "name": "user_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false @@ -23,14 +23,14 @@ }, "provider": { "name": "provider", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false }, "provider_account_id": { "name": "provider_account_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false @@ -51,7 +51,7 @@ }, "expires_at": { "name": "expires_at", - "type": "integer", + "type": "int", "primaryKey": false, "notNull": false, "autoincrement": false @@ -111,23 +111,23 @@ }, "compositePrimaryKeys": { "account_provider_provider_account_id_pk": { + "name": "account_provider_provider_account_id_pk", "columns": [ "provider", "provider_account_id" - ], - "name": "account_provider_provider_account_id_pk" + ] } }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "apiKey": { "name": "apiKey", "columns": { "id": { "name": "id", - "type": "text", - "primaryKey": true, + "type": "varchar(64)", + "primaryKey": false, "notNull": true, "autoincrement": false }, @@ -140,7 +140,7 @@ }, "user_id": { "name": "user_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false @@ -162,17 +162,24 @@ "onUpdate": "no action" } }, - "compositePrimaryKeys": {}, + "compositePrimaryKeys": { + "apiKey_id": { + "name": "apiKey_id", + "columns": [ + "id" + ] + } + }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "app": { "name": "app", "columns": { "id": { "name": "id", - "type": "text", - "primaryKey": true, + "type": "varchar(64)", + "primaryKey": false, "notNull": true, "autoincrement": false }, @@ -214,30 +221,37 @@ }, "indexes": {}, "foreignKeys": {}, - "compositePrimaryKeys": {}, + "compositePrimaryKeys": { + "app_id": { + "name": "app_id", + "columns": [ + "id" + ] + } + }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "boardGroupPermission": { "name": "boardGroupPermission", "columns": { "board_id": { "name": "board_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false }, "group_id": { "name": "group_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false }, "permission": { "name": "permission", - "type": "text", + "type": "varchar(128)", "primaryKey": false, "notNull": true, "autoincrement": false @@ -274,37 +288,37 @@ }, "compositePrimaryKeys": { "boardGroupPermission_board_id_group_id_permission_pk": { + "name": "boardGroupPermission_board_id_group_id_permission_pk", "columns": [ "board_id", "group_id", "permission" - ], - "name": "boardGroupPermission_board_id_group_id_permission_pk" + ] } }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "boardUserPermission": { "name": "boardUserPermission", "columns": { "board_id": { "name": "board_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false }, "user_id": { "name": "user_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false }, "permission": { "name": "permission", - "type": "text", + "type": "varchar(128)", "primaryKey": false, "notNull": true, "autoincrement": false @@ -341,37 +355,37 @@ }, "compositePrimaryKeys": { "boardUserPermission_board_id_user_id_permission_pk": { + "name": "boardUserPermission_board_id_user_id_permission_pk", "columns": [ "board_id", "user_id", "permission" - ], - "name": "boardUserPermission_board_id_user_id_permission_pk" + ] } }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "board": { "name": "board", "columns": { "id": { "name": "id", - "type": "text", - "primaryKey": true, + "type": "varchar(64)", + "primaryKey": false, "notNull": true, "autoincrement": false }, "name": { "name": "name", - "type": "text", + "type": "varchar(256)", "primaryKey": false, "notNull": true, "autoincrement": false }, "is_public": { "name": "is_public", - "type": "integer", + "type": "boolean", "primaryKey": false, "notNull": true, "autoincrement": false, @@ -379,7 +393,7 @@ }, "creator_id": { "name": "creator_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": false, "autoincrement": false @@ -425,7 +439,7 @@ "primaryKey": false, "notNull": true, "autoincrement": false, - "default": "'fixed'" + "default": "('fixed')" }, "background_image_repeat": { "name": "background_image_repeat", @@ -433,7 +447,7 @@ "primaryKey": false, "notNull": true, "autoincrement": false, - "default": "'no-repeat'" + "default": "('no-repeat')" }, "background_image_size": { "name": "background_image_size", @@ -441,7 +455,7 @@ "primaryKey": false, "notNull": true, "autoincrement": false, - "default": "'cover'" + "default": "('cover')" }, "primary_color": { "name": "primary_color", @@ -449,7 +463,7 @@ "primaryKey": false, "notNull": true, "autoincrement": false, - "default": "'#fa5252'" + "default": "('#fa5252')" }, "secondary_color": { "name": "secondary_color", @@ -457,11 +471,11 @@ "primaryKey": false, "notNull": true, "autoincrement": false, - "default": "'#fd7e14'" + "default": "('#fd7e14')" }, "opacity": { "name": "opacity", - "type": "integer", + "type": "int", "primaryKey": false, "notNull": true, "autoincrement": false, @@ -487,26 +501,18 @@ "primaryKey": false, "notNull": true, "autoincrement": false, - "default": "'lg'" + "default": "('lg')" }, "disable_status": { "name": "disable_status", - "type": "integer", + "type": "boolean", "primaryKey": false, "notNull": true, "autoincrement": false, "default": false } }, - "indexes": { - "board_name_unique": { - "name": "board_name_unique", - "columns": [ - "name" - ], - "isUnique": true - } - }, + "indexes": {}, "foreignKeys": { "board_creator_id_user_id_fk": { "name": "board_creator_id_user_id_fk", @@ -522,30 +528,44 @@ "onUpdate": "no action" } }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} + "compositePrimaryKeys": { + "board_id": { + "name": "board_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "board_name_unique": { + "name": "board_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} }, "cron_job_configuration": { "name": "cron_job_configuration", "columns": { "name": { "name": "name", - "type": "text", - "primaryKey": true, + "type": "varchar(256)", + "primaryKey": false, "notNull": true, "autoincrement": false }, "cron_expression": { "name": "cron_expression", - "type": "text", + "type": "varchar(32)", "primaryKey": false, "notNull": true, "autoincrement": false }, "is_enabled": { "name": "is_enabled", - "type": "integer", + "type": "boolean", "primaryKey": false, "notNull": true, "autoincrement": false, @@ -554,23 +574,30 @@ }, "indexes": {}, "foreignKeys": {}, - "compositePrimaryKeys": {}, + "compositePrimaryKeys": { + "cron_job_configuration_name": { + "name": "cron_job_configuration_name", + "columns": [ + "name" + ] + } + }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "custom_widget_definition": { "name": "custom_widget_definition", "columns": { "id": { "name": "id", - "type": "text", - "primaryKey": true, + "type": "varchar(64)", + "primaryKey": false, "notNull": true, "autoincrement": false }, "name": { "name": "name", - "type": "text", + "type": "varchar(256)", "primaryKey": false, "notNull": true, "autoincrement": false @@ -589,16 +616,9 @@ "notNull": false, "autoincrement": false }, - "url": { - "name": "url", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, "auth_type": { "name": "auth_type", - "type": "text", + "type": "varchar(32)", "primaryKey": false, "notNull": true, "autoincrement": false, @@ -606,14 +626,14 @@ }, "header_name": { "name": "header_name", - "type": "text", + "type": "varchar(256)", "primaryKey": false, "notNull": false, "autoincrement": false }, "method": { "name": "method", - "type": "text", + "type": "varchar(16)", "primaryKey": false, "notNull": true, "autoincrement": false, @@ -628,7 +648,7 @@ }, "display_type": { "name": "display_type", - "type": "text", + "type": "varchar(32)", "primaryKey": false, "notNull": true, "autoincrement": false, @@ -640,38 +660,45 @@ "primaryKey": false, "notNull": true, "autoincrement": false, - "default": "'{\"json\": {}}'" + "default": "('{\"json\": {}}')" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false }, "enabled": { "name": "enabled", - "type": "integer", + "type": "boolean", "primaryKey": false, "notNull": true, "autoincrement": false, "default": true - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - }, - "creator_id": { - "name": "creator_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false } }, "indexes": {}, @@ -690,16 +717,23 @@ "onUpdate": "no action" } }, - "compositePrimaryKeys": {}, + "compositePrimaryKeys": { + "custom_widget_definition_id": { + "name": "custom_widget_definition_id", + "columns": [ + "id" + ] + } + }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "custom_widget_secret": { "name": "custom_widget_secret", "columns": { "kind": { "name": "kind", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false @@ -713,14 +747,14 @@ }, "updated_at": { "name": "updated_at", - "type": "integer", + "type": "timestamp", "primaryKey": false, "notNull": true, "autoincrement": false }, "definition_id": { "name": "definition_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false @@ -728,8 +762,8 @@ }, "indexes": {}, "foreignKeys": { - "custom_widget_secret_definition_id_custom_widget_definition_id_fk": { - "name": "custom_widget_secret_definition_id_custom_widget_definition_id_fk", + "cw_secret_definition_id_cw_definition_id_fk": { + "name": "cw_secret_definition_id_cw_definition_id_fk", "tableFrom": "custom_widget_secret", "tableTo": "custom_widget_definition", "columnsFrom": [ @@ -744,29 +778,29 @@ }, "compositePrimaryKeys": { "custom_widget_secret_definition_id_kind_pk": { + "name": "custom_widget_secret_definition_id_kind_pk", "columns": [ "definition_id", "kind" - ], - "name": "custom_widget_secret_definition_id_kind_pk" + ] } }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "groupMember": { "name": "groupMember", "columns": { "group_id": { "name": "group_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false }, "user_id": { "name": "user_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false @@ -803,22 +837,22 @@ }, "compositePrimaryKeys": { "groupMember_group_id_user_id_pk": { + "name": "groupMember_group_id_user_id_pk", "columns": [ "group_id", "user_id" - ], - "name": "groupMember_group_id_user_id_pk" + ] } }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "groupPermission": { "name": "groupPermission", "columns": { "group_id": { "name": "group_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false @@ -849,63 +883,55 @@ }, "compositePrimaryKeys": {}, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "group": { "name": "group", "columns": { "id": { "name": "id", - "type": "text", - "primaryKey": true, + "type": "varchar(64)", + "primaryKey": false, "notNull": true, "autoincrement": false }, "name": { "name": "name", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false }, "owner_id": { "name": "owner_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": false, "autoincrement": false }, "home_board_id": { "name": "home_board_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": false, "autoincrement": false }, "mobile_home_board_id": { "name": "mobile_home_board_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": false, "autoincrement": false }, "position": { "name": "position", - "type": "integer", + "type": "smallint", "primaryKey": false, "notNull": true, "autoincrement": false } }, - "indexes": { - "group_name_unique": { - "name": "group_name_unique", - "columns": [ - "name" - ], - "isUnique": true - } - }, + "indexes": {}, "foreignKeys": { "group_owner_id_user_id_fk": { "name": "group_owner_id_user_id_fk", @@ -947,23 +973,37 @@ "onUpdate": "no action" } }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} + "compositePrimaryKeys": { + "group_id": { + "name": "group_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "group_name_unique": { + "name": "group_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} }, "iconRepository": { "name": "iconRepository", "columns": { "id": { "name": "id", - "type": "text", - "primaryKey": true, + "type": "varchar(64)", + "primaryKey": false, "notNull": true, "autoincrement": false }, "slug": { "name": "slug", - "type": "text", + "type": "varchar(150)", "primaryKey": false, "notNull": true, "autoincrement": false @@ -971,23 +1011,30 @@ }, "indexes": {}, "foreignKeys": {}, - "compositePrimaryKeys": {}, + "compositePrimaryKeys": { + "iconRepository_id": { + "name": "iconRepository_id", + "columns": [ + "id" + ] + } + }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "icon": { "name": "icon", "columns": { "id": { "name": "id", - "type": "text", - "primaryKey": true, + "type": "varchar(64)", + "primaryKey": false, "notNull": true, "autoincrement": false }, "name": { "name": "name", - "type": "text", + "type": "varchar(250)", "primaryKey": false, "notNull": true, "autoincrement": false @@ -1008,7 +1055,7 @@ }, "icon_repository_id": { "name": "icon_repository_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false @@ -1030,30 +1077,37 @@ "onUpdate": "no action" } }, - "compositePrimaryKeys": {}, + "compositePrimaryKeys": { + "icon_id": { + "name": "icon_id", + "columns": [ + "id" + ] + } + }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "integrationGroupPermissions": { "name": "integrationGroupPermissions", "columns": { "integration_id": { "name": "integration_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false }, "group_id": { "name": "group_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false }, "permission": { "name": "permission", - "type": "text", + "type": "varchar(128)", "primaryKey": false, "notNull": true, "autoincrement": false @@ -1089,31 +1143,31 @@ } }, "compositePrimaryKeys": { - "integrationGroupPermissions_integration_id_group_id_permission_pk": { + "integration_group_permission__pk": { + "name": "integration_group_permission__pk", "columns": [ "integration_id", "group_id", "permission" - ], - "name": "integrationGroupPermissions_integration_id_group_id_permission_pk" + ] } }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "integration_item": { "name": "integration_item", "columns": { "item_id": { "name": "item_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false }, "integration_id": { "name": "integration_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false @@ -1150,22 +1204,22 @@ }, "compositePrimaryKeys": { "integration_item_item_id_integration_id_pk": { + "name": "integration_item_item_id_integration_id_pk", "columns": [ "item_id", "integration_id" - ], - "name": "integration_item_item_id_integration_id_pk" + ] } }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "integrationSecret": { "name": "integrationSecret", "columns": { "kind": { "name": "kind", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false @@ -1179,14 +1233,14 @@ }, "updated_at": { "name": "updated_at", - "type": "integer", + "type": "timestamp", "primaryKey": false, "notNull": true, "autoincrement": false }, "integration_id": { "name": "integration_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false @@ -1225,36 +1279,36 @@ }, "compositePrimaryKeys": { "integrationSecret_integration_id_kind_pk": { + "name": "integrationSecret_integration_id_kind_pk", "columns": [ "integration_id", "kind" - ], - "name": "integrationSecret_integration_id_kind_pk" + ] } }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "integrationUserPermission": { "name": "integrationUserPermission", "columns": { "integration_id": { "name": "integration_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false }, "user_id": { "name": "user_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false }, "permission": { "name": "permission", - "type": "text", + "type": "varchar(128)", "primaryKey": false, "notNull": true, "autoincrement": false @@ -1291,24 +1345,24 @@ }, "compositePrimaryKeys": { "integrationUserPermission_integration_id_user_id_permission_pk": { + "name": "integrationUserPermission_integration_id_user_id_permission_pk", "columns": [ "integration_id", "user_id", "permission" - ], - "name": "integrationUserPermission_integration_id_user_id_permission_pk" + ] } }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "integration": { "name": "integration", "columns": { "id": { "name": "id", - "type": "text", - "primaryKey": true, + "type": "varchar(64)", + "primaryKey": false, "notNull": true, "autoincrement": false }, @@ -1328,21 +1382,14 @@ }, "kind": { "name": "kind", - "type": "text", + "type": "varchar(128)", "primaryKey": false, "notNull": true, "autoincrement": false }, "app_id": { "name": "app_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "creator_id": { - "name": "creator_id", - "type": "text", + "type": "varchar(128)", "primaryKey": false, "notNull": false, "autoincrement": false @@ -1370,66 +1417,52 @@ ], "onDelete": "set null", "onUpdate": "no action" - }, - "integration_creator_id_user_id_fk": { - "name": "integration_creator_id_user_id_fk", - "tableFrom": "integration", - "tableTo": "user", - "columnsFrom": [ - "creator_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" } }, - "compositePrimaryKeys": {}, + "compositePrimaryKeys": { + "integration_id": { + "name": "integration_id", + "columns": [ + "id" + ] + } + }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "invite": { "name": "invite", "columns": { "id": { "name": "id", - "type": "text", - "primaryKey": true, + "type": "varchar(64)", + "primaryKey": false, "notNull": true, "autoincrement": false }, "token": { "name": "token", - "type": "text", + "type": "varchar(512)", "primaryKey": false, "notNull": true, "autoincrement": false }, "expiration_date": { "name": "expiration_date", - "type": "integer", + "type": "timestamp", "primaryKey": false, "notNull": true, "autoincrement": false }, "creator_id": { "name": "creator_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false } }, - "indexes": { - "invite_token_unique": { - "name": "invite_token_unique", - "columns": [ - "token" - ], - "isUnique": true - } - }, + "indexes": {}, "foreignKeys": { "invite_creator_id_user_id_fk": { "name": "invite_creator_id_user_id_fk", @@ -1445,58 +1478,72 @@ "onUpdate": "no action" } }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} + "compositePrimaryKeys": { + "invite_id": { + "name": "invite_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "invite_token_unique": { + "name": "invite_token_unique", + "columns": [ + "token" + ] + } + }, + "checkConstraint": {} }, "item_layout": { "name": "item_layout", "columns": { "item_id": { "name": "item_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false }, "section_id": { "name": "section_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false }, "layout_id": { "name": "layout_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false }, "x_offset": { "name": "x_offset", - "type": "integer", + "type": "int", "primaryKey": false, "notNull": true, "autoincrement": false }, "y_offset": { "name": "y_offset", - "type": "integer", + "type": "int", "primaryKey": false, "notNull": true, "autoincrement": false }, "width": { "name": "width", - "type": "integer", + "type": "int", "primaryKey": false, "notNull": true, "autoincrement": false }, "height": { "name": "height", - "type": "integer", + "type": "int", "primaryKey": false, "notNull": true, "autoincrement": false @@ -1546,30 +1593,30 @@ }, "compositePrimaryKeys": { "item_layout_item_id_section_id_layout_id_pk": { + "name": "item_layout_item_id_section_id_layout_id_pk", "columns": [ "item_id", "section_id", "layout_id" - ], - "name": "item_layout_item_id_section_id_layout_id_pk" + ] } }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "item": { "name": "item", "columns": { "id": { "name": "id", - "type": "text", - "primaryKey": true, + "type": "varchar(64)", + "primaryKey": false, "notNull": true, "autoincrement": false }, "board_id": { "name": "board_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false @@ -1587,7 +1634,7 @@ "primaryKey": false, "notNull": true, "autoincrement": false, - "default": "'{\"json\": {}}'" + "default": "('{\"json\": {}}')" }, "advanced_options": { "name": "advanced_options", @@ -1595,7 +1642,7 @@ "primaryKey": false, "notNull": true, "autoincrement": false, - "default": "'{\"json\": {}}'" + "default": "('{\"json\": {}}')" } }, "indexes": {}, @@ -1614,44 +1661,51 @@ "onUpdate": "no action" } }, - "compositePrimaryKeys": {}, + "compositePrimaryKeys": { + "item_id": { + "name": "item_id", + "columns": [ + "id" + ] + } + }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "layout": { "name": "layout", "columns": { "id": { "name": "id", - "type": "text", - "primaryKey": true, + "type": "varchar(64)", + "primaryKey": false, "notNull": true, "autoincrement": false }, "name": { "name": "name", - "type": "text", + "type": "varchar(32)", "primaryKey": false, "notNull": true, "autoincrement": false }, "board_id": { "name": "board_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false }, "column_count": { "name": "column_count", - "type": "integer", + "type": "tinyint", "primaryKey": false, "notNull": true, "autoincrement": false }, "breakpoint": { "name": "breakpoint", - "type": "integer", + "type": "smallint", "primaryKey": false, "notNull": true, "autoincrement": false, @@ -1674,30 +1728,37 @@ "onUpdate": "no action" } }, - "compositePrimaryKeys": {}, + "compositePrimaryKeys": { + "layout_id": { + "name": "layout_id", + "columns": [ + "id" + ] + } + }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "media": { "name": "media", "columns": { "id": { "name": "id", - "type": "text", - "primaryKey": true, + "type": "varchar(64)", + "primaryKey": false, "notNull": true, "autoincrement": false }, "name": { "name": "name", - "type": "text", + "type": "varchar(512)", "primaryKey": false, "notNull": true, "autoincrement": false }, "content": { "name": "content", - "type": "blob", + "type": "LONGBLOB", "primaryKey": false, "notNull": true, "autoincrement": false @@ -1711,22 +1772,22 @@ }, "size": { "name": "size", - "type": "integer", + "type": "int", "primaryKey": false, "notNull": true, "autoincrement": false }, "created_at": { "name": "created_at", - "type": "integer", + "type": "timestamp", "primaryKey": false, "notNull": true, "autoincrement": false, - "default": "(unixepoch())" + "default": "(now())" }, "creator_id": { "name": "creator_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": false, "autoincrement": false @@ -1748,30 +1809,37 @@ "onUpdate": "no action" } }, - "compositePrimaryKeys": {}, + "compositePrimaryKeys": { + "media_id": { + "name": "media_id", + "columns": [ + "id" + ] + } + }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "onboarding": { "name": "onboarding", "columns": { "id": { "name": "id", - "type": "text", - "primaryKey": true, + "type": "varchar(64)", + "primaryKey": false, "notNull": true, "autoincrement": false }, "step": { "name": "step", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false }, "previous_step": { "name": "previous_step", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": false, "autoincrement": false @@ -1779,17 +1847,24 @@ }, "indexes": {}, "foreignKeys": {}, - "compositePrimaryKeys": {}, + "compositePrimaryKeys": { + "onboarding_id": { + "name": "onboarding_id", + "columns": [ + "id" + ] + } + }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "search_engine": { "name": "search_engine", "columns": { "id": { "name": "id", - "type": "text", - "primaryKey": true, + "type": "varchar(64)", + "primaryKey": false, "notNull": true, "autoincrement": false }, @@ -1802,14 +1877,14 @@ }, "name": { "name": "name", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false }, "short": { "name": "short", - "type": "text", + "type": "varchar(8)", "primaryKey": false, "notNull": true, "autoincrement": false @@ -1830,7 +1905,7 @@ }, "type": { "name": "type", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false, @@ -1838,21 +1913,13 @@ }, "integration_id": { "name": "integration_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": false, "autoincrement": false } }, - "indexes": { - "search_engine_short_unique": { - "name": "search_engine_short_unique", - "columns": [ - "short" - ], - "isUnique": true - } - }, + "indexes": {}, "foreignKeys": { "search_engine_integration_id_integration_id_fk": { "name": "search_engine_integration_id_integration_id_fk", @@ -1868,30 +1935,44 @@ "onUpdate": "no action" } }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} + "compositePrimaryKeys": { + "search_engine_id": { + "name": "search_engine_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "search_engine_short_unique": { + "name": "search_engine_short_unique", + "columns": [ + "short" + ] + } + }, + "checkConstraint": {} }, "section_collapse_state": { "name": "section_collapse_state", "columns": { "user_id": { "name": "user_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false }, "section_id": { "name": "section_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false }, "collapsed": { "name": "collapsed", - "type": "integer", + "type": "boolean", "primaryKey": false, "notNull": true, "autoincrement": false, @@ -1929,64 +2010,64 @@ }, "compositePrimaryKeys": { "section_collapse_state_user_id_section_id_pk": { + "name": "section_collapse_state_user_id_section_id_pk", "columns": [ "user_id", "section_id" - ], - "name": "section_collapse_state_user_id_section_id_pk" + ] } }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "section_layout": { "name": "section_layout", "columns": { "section_id": { "name": "section_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false }, "layout_id": { "name": "layout_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false }, "parent_section_id": { "name": "parent_section_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": false, "autoincrement": false }, "x_offset": { "name": "x_offset", - "type": "integer", + "type": "int", "primaryKey": false, "notNull": true, "autoincrement": false }, "y_offset": { "name": "y_offset", - "type": "integer", + "type": "int", "primaryKey": false, "notNull": true, "autoincrement": false }, "width": { "name": "width", - "type": "integer", + "type": "int", "primaryKey": false, "notNull": true, "autoincrement": false }, "height": { "name": "height", - "type": "integer", + "type": "int", "primaryKey": false, "notNull": true, "autoincrement": false @@ -2036,29 +2117,29 @@ }, "compositePrimaryKeys": { "section_layout_section_id_layout_id_pk": { + "name": "section_layout_section_id_layout_id_pk", "columns": [ "section_id", "layout_id" - ], - "name": "section_layout_section_id_layout_id_pk" + ] } }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "section": { "name": "section", "columns": { "id": { "name": "id", - "type": "text", - "primaryKey": true, + "type": "varchar(64)", + "primaryKey": false, "notNull": true, "autoincrement": false }, "board_id": { "name": "board_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false @@ -2072,14 +2153,14 @@ }, "x_offset": { "name": "x_offset", - "type": "integer", + "type": "int", "primaryKey": false, "notNull": false, "autoincrement": false }, "y_offset": { "name": "y_offset", - "type": "integer", + "type": "int", "primaryKey": false, "notNull": false, "autoincrement": false @@ -2097,7 +2178,7 @@ "primaryKey": false, "notNull": false, "autoincrement": false, - "default": "'{\"json\": {}}'" + "default": "('{\"json\": {}}')" } }, "indexes": {}, @@ -2116,17 +2197,24 @@ "onUpdate": "no action" } }, - "compositePrimaryKeys": {}, + "compositePrimaryKeys": { + "section_id": { + "name": "section_id", + "columns": [ + "id" + ] + } + }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "serverSetting": { "name": "serverSetting", "columns": { "setting_key": { "name": "setting_key", - "type": "text", - "primaryKey": true, + "type": "varchar(64)", + "primaryKey": false, "notNull": true, "autoincrement": false }, @@ -2136,43 +2224,49 @@ "primaryKey": false, "notNull": true, "autoincrement": false, - "default": "'{\"json\": {}}'" + "default": "('{\"json\": {}}')" } }, - "indexes": { + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "serverSetting_setting_key": { + "name": "serverSetting_setting_key", + "columns": [ + "setting_key" + ] + } + }, + "uniqueConstraints": { "serverSetting_settingKey_unique": { "name": "serverSetting_settingKey_unique", "columns": [ "setting_key" - ], - "isUnique": true + ] } }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "session": { "name": "session", "columns": { "session_token": { "name": "session_token", - "type": "text", - "primaryKey": true, + "type": "varchar(512)", + "primaryKey": false, "notNull": true, "autoincrement": false }, "user_id": { "name": "user_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false }, "expires": { "name": "expires", - "type": "integer", + "type": "timestamp", "primaryKey": false, "notNull": true, "autoincrement": false @@ -2202,56 +2296,24 @@ "onUpdate": "no action" } }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "trusted_certificate_hostname": { - "name": "trusted_certificate_hostname", - "columns": { - "hostname": { - "name": "hostname", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "thumbprint": { - "name": "thumbprint", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "certificate": { - "name": "certificate", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, "compositePrimaryKeys": { - "trusted_certificate_hostname_hostname_thumbprint_pk": { + "session_session_token": { + "name": "session_session_token", "columns": [ - "hostname", - "thumbprint" - ], - "name": "trusted_certificate_hostname_hostname_thumbprint_pk" + "session_token" + ] } }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "user": { "name": "user", "columns": { "id": { "name": "id", - "type": "text", - "primaryKey": true, + "type": "varchar(64)", + "primaryKey": false, "notNull": true, "autoincrement": false }, @@ -2271,7 +2333,7 @@ }, "email_verified": { "name": "email_verified", - "type": "integer", + "type": "timestamp", "primaryKey": false, "notNull": false, "autoincrement": false @@ -2292,7 +2354,7 @@ }, "provider": { "name": "provider", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false, @@ -2300,36 +2362,36 @@ }, "home_board_id": { "name": "home_board_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": false, "autoincrement": false }, "mobile_home_board_id": { "name": "mobile_home_board_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": false, "autoincrement": false }, "default_search_engine_id": { "name": "default_search_engine_id", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": false, "autoincrement": false }, "open_search_in_new_tab": { "name": "open_search_in_new_tab", - "type": "integer", + "type": "boolean", "primaryKey": false, "notNull": true, "autoincrement": false, - "default": true + "default": false }, "ddg_bangs": { "name": "ddg_bangs", - "type": "integer", + "type": "boolean", "primaryKey": false, "notNull": true, "autoincrement": false, @@ -2337,7 +2399,7 @@ }, "color_scheme": { "name": "color_scheme", - "type": "text", + "type": "varchar(5)", "primaryKey": false, "notNull": true, "autoincrement": false, @@ -2345,7 +2407,7 @@ }, "first_day_of_week": { "name": "first_day_of_week", - "type": "integer", + "type": "tinyint", "primaryKey": false, "notNull": true, "autoincrement": false, @@ -2353,7 +2415,7 @@ }, "ping_icons_enabled": { "name": "ping_icons_enabled", - "type": "integer", + "type": "boolean", "primaryKey": false, "notNull": true, "autoincrement": false, @@ -2361,7 +2423,7 @@ }, "completed_manage_tour": { "name": "completed_manage_tour", - "type": "integer", + "type": "boolean", "primaryKey": false, "notNull": true, "autoincrement": false, @@ -2369,7 +2431,7 @@ }, "completed_board_tour": { "name": "completed_board_tour", - "type": "integer", + "type": "boolean", "primaryKey": false, "notNull": true, "autoincrement": false, @@ -2418,30 +2480,37 @@ "onUpdate": "no action" } }, - "compositePrimaryKeys": {}, + "compositePrimaryKeys": { + "user_id": { + "name": "user_id", + "columns": [ + "id" + ] + } + }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} }, "verificationToken": { "name": "verificationToken", "columns": { "identifier": { "name": "identifier", - "type": "text", + "type": "varchar(64)", "primaryKey": false, "notNull": true, "autoincrement": false }, "token": { "name": "token", - "type": "text", + "type": "varchar(512)", "primaryKey": false, "notNull": true, "autoincrement": false }, "expires": { "name": "expires", - "type": "integer", + "type": "timestamp", "primaryKey": false, "notNull": true, "autoincrement": false @@ -2451,25 +2520,64 @@ "foreignKeys": {}, "compositePrimaryKeys": { "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", "columns": [ "identifier", "token" - ], - "name": "verificationToken_identifier_token_pk" + ] } }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} + }, + "trusted_certificate_hostname": { + "name": "trusted_certificate_hostname", + "columns": { + "hostname": { + "name": "hostname", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "thumbprint": { + "name": "thumbprint", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "certificate": { + "name": "certificate", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "trusted_certificate_hostname_hostname_thumbprint_pk": { + "name": "trusted_certificate_hostname_hostname_thumbprint_pk", + "columns": [ + "hostname", + "thumbprint" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} } }, "views": {}, - "enums": {}, "_meta": { "schemas": {}, "tables": {}, "columns": {} }, "internal": { + "tables": {}, "indexes": {} } -} \ No newline at end of file +} diff --git a/packages/db/migrations/mysql/meta/0042_snapshot.json b/packages/db/migrations/mysql/meta/0042_snapshot.json new file mode 100644 index 000000000..945d949c5 --- /dev/null +++ b/packages/db/migrations/mysql/meta/0042_snapshot.json @@ -0,0 +1,2643 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "4df22d3b-b69e-4afc-9d82-ac73d3c6e100", + "prevId": "8e2ec048-e89a-4d46-84ee-ae3da138b5ab", + "tables": { + "account": { + "name": "account", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "userId_idx": { + "name": "userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_provider_account_id_pk": { + "name": "account_provider_provider_account_id_pk", + "columns": [ + "provider", + "provider_account_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "apiKey": { + "name": "apiKey", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "apiKey_user_id_user_id_fk": { + "name": "apiKey_user_id_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "apiKey_id": { + "name": "apiKey_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "app": { + "name": "app", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "href": { + "name": "href", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ping_url": { + "name": "ping_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "app_id": { + "name": "app_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "boardGroupPermission": { + "name": "boardGroupPermission", + "columns": { + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "boardGroupPermission_board_id_board_id_fk": { + "name": "boardGroupPermission_board_id_board_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardGroupPermission_group_id_group_id_fk": { + "name": "boardGroupPermission_group_id_group_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardGroupPermission_board_id_group_id_permission_pk": { + "name": "boardGroupPermission_board_id_group_id_permission_pk", + "columns": [ + "board_id", + "group_id", + "permission" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "boardUserPermission": { + "name": "boardUserPermission", + "columns": { + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "boardUserPermission_board_id_board_id_fk": { + "name": "boardUserPermission_board_id_board_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardUserPermission_user_id_user_id_fk": { + "name": "boardUserPermission_user_id_user_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardUserPermission_board_id_user_id_permission_pk": { + "name": "boardUserPermission_board_id_user_id_permission_pk", + "columns": [ + "board_id", + "user_id", + "permission" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "board": { + "name": "board", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "page_title": { + "name": "page_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "meta_title": { + "name": "meta_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logo_image_url": { + "name": "logo_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "favicon_image_url": { + "name": "favicon_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "background_image_url": { + "name": "background_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "background_image_attachment": { + "name": "background_image_attachment", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('fixed')" + }, + "background_image_repeat": { + "name": "background_image_repeat", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('no-repeat')" + }, + "background_image_size": { + "name": "background_image_size", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('cover')" + }, + "primary_color": { + "name": "primary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('#fa5252')" + }, + "secondary_color": { + "name": "secondary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('#fd7e14')" + }, + "opacity": { + "name": "opacity", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 100 + }, + "custom_css": { + "name": "custom_css", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon_color": { + "name": "icon_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "item_radius": { + "name": "item_radius", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('lg')" + }, + "disable_status": { + "name": "disable_status", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "board_creator_id_user_id_fk": { + "name": "board_creator_id_user_id_fk", + "tableFrom": "board", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "board_id": { + "name": "board_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "board_name_unique": { + "name": "board_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "cron_job_configuration": { + "name": "cron_job_configuration", + "columns": { + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "cron_job_configuration_name": { + "name": "cron_job_configuration_name", + "columns": [ + "name" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "custom_widget_definition": { + "name": "custom_widget_definition", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "header_name": { + "name": "header_name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'GET'" + }, + "request_body": { + "name": "request_body", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_type": { + "name": "display_type", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'singleValue'" + }, + "display_config": { + "name": "display_config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{\"json\": {}}')" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "custom_widget_definition_creator_id_user_id_fk": { + "name": "custom_widget_definition_creator_id_user_id_fk", + "tableFrom": "custom_widget_definition", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "custom_widget_definition_id": { + "name": "custom_widget_definition_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "custom_widget_secret": { + "name": "custom_widget_secret", + "columns": { + "kind": { + "name": "kind", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "definition_id": { + "name": "definition_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "cw_secret_definition_id_cw_definition_id_fk": { + "name": "cw_secret_definition_id_cw_definition_id_fk", + "tableFrom": "custom_widget_secret", + "tableTo": "custom_widget_definition", + "columnsFrom": [ + "definition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "custom_widget_secret_definition_id_kind_pk": { + "name": "custom_widget_secret_definition_id_kind_pk", + "columns": [ + "definition_id", + "kind" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "groupMember": { + "name": "groupMember", + "columns": { + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "groupMember_group_id_group_id_fk": { + "name": "groupMember_group_id_group_id_fk", + "tableFrom": "groupMember", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "groupMember_user_id_user_id_fk": { + "name": "groupMember_user_id_user_id_fk", + "tableFrom": "groupMember", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "groupMember_group_id_user_id_pk": { + "name": "groupMember_group_id_user_id_pk", + "columns": [ + "group_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "groupPermission": { + "name": "groupPermission", + "columns": { + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "groupPermission_group_id_group_id_fk": { + "name": "groupPermission_group_id_group_id_fk", + "tableFrom": "groupPermission", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "group": { + "name": "group", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "home_board_id": { + "name": "home_board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mobile_home_board_id": { + "name": "mobile_home_board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "group_owner_id_user_id_fk": { + "name": "group_owner_id_user_id_fk", + "tableFrom": "group", + "tableTo": "user", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "group_home_board_id_board_id_fk": { + "name": "group_home_board_id_board_id_fk", + "tableFrom": "group", + "tableTo": "board", + "columnsFrom": [ + "home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "group_mobile_home_board_id_board_id_fk": { + "name": "group_mobile_home_board_id_board_id_fk", + "tableFrom": "group", + "tableTo": "board", + "columnsFrom": [ + "mobile_home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "group_id": { + "name": "group_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "group_name_unique": { + "name": "group_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "iconRepository": { + "name": "iconRepository", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(150)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "iconRepository_id": { + "name": "iconRepository_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "icon": { + "name": "icon", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(250)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon_repository_id": { + "name": "icon_repository_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "icon_icon_repository_id_iconRepository_id_fk": { + "name": "icon_icon_repository_id_iconRepository_id_fk", + "tableFrom": "icon", + "tableTo": "iconRepository", + "columnsFrom": [ + "icon_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "icon_id": { + "name": "icon_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "integrationGroupPermissions": { + "name": "integrationGroupPermissions", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integrationGroupPermissions_integration_id_integration_id_fk": { + "name": "integrationGroupPermissions_integration_id_integration_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationGroupPermissions_group_id_group_id_fk": { + "name": "integrationGroupPermissions_group_id_group_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_group_permission__pk": { + "name": "integration_group_permission__pk", + "columns": [ + "integration_id", + "group_id", + "permission" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "integration_item": { + "name": "integration_item", + "columns": { + "item_id": { + "name": "item_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integration_item_item_id_item_id_fk": { + "name": "integration_item_item_id_item_id_fk", + "tableFrom": "integration_item", + "tableTo": "item", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integration_item_integration_id_integration_id_fk": { + "name": "integration_item_integration_id_integration_id_fk", + "tableFrom": "integration_item", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_item_item_id_integration_id_pk": { + "name": "integration_item_item_id_integration_id_pk", + "columns": [ + "item_id", + "integration_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "integrationSecret": { + "name": "integrationSecret", + "columns": { + "kind": { + "name": "kind", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "integration_secret__kind_idx": { + "name": "integration_secret__kind_idx", + "columns": [ + "kind" + ], + "isUnique": false + }, + "integration_secret__updated_at_idx": { + "name": "integration_secret__updated_at_idx", + "columns": [ + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "integrationSecret_integration_id_integration_id_fk": { + "name": "integrationSecret_integration_id_integration_id_fk", + "tableFrom": "integrationSecret", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationSecret_integration_id_kind_pk": { + "name": "integrationSecret_integration_id_kind_pk", + "columns": [ + "integration_id", + "kind" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "integrationUserPermission": { + "name": "integrationUserPermission", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integrationUserPermission_integration_id_integration_id_fk": { + "name": "integrationUserPermission_integration_id_integration_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationUserPermission_user_id_user_id_fk": { + "name": "integrationUserPermission_user_id_user_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationUserPermission_integration_id_user_id_permission_pk": { + "name": "integrationUserPermission_integration_id_user_id_permission_pk", + "columns": [ + "integration_id", + "user_id", + "permission" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "integration": { + "name": "integration", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "integration__kind_idx": { + "name": "integration__kind_idx", + "columns": [ + "kind" + ], + "isUnique": false + } + }, + "foreignKeys": { + "integration_app_id_app_id_fk": { + "name": "integration_app_id_app_id_fk", + "tableFrom": "integration", + "tableTo": "app", + "columnsFrom": [ + "app_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_id": { + "name": "integration_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "invite": { + "name": "invite", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expiration_date": { + "name": "expiration_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "invite_creator_id_user_id_fk": { + "name": "invite_creator_id_user_id_fk", + "tableFrom": "invite", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "invite_id": { + "name": "invite_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "invite_token_unique": { + "name": "invite_token_unique", + "columns": [ + "token" + ] + } + }, + "checkConstraint": {} + }, + "item_layout": { + "name": "item_layout", + "columns": { + "item_id": { + "name": "item_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout_id": { + "name": "layout_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "x_offset": { + "name": "x_offset", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "y_offset": { + "name": "y_offset", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "item_layout_item_id_item_id_fk": { + "name": "item_layout_item_id_item_id_fk", + "tableFrom": "item_layout", + "tableTo": "item", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "item_layout_section_id_section_id_fk": { + "name": "item_layout_section_id_section_id_fk", + "tableFrom": "item_layout", + "tableTo": "section", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "item_layout_layout_id_layout_id_fk": { + "name": "item_layout_layout_id_layout_id_fk", + "tableFrom": "item_layout", + "tableTo": "layout", + "columnsFrom": [ + "layout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "item_layout_item_id_section_id_layout_id_pk": { + "name": "item_layout_item_id_section_id_layout_id_pk", + "columns": [ + "item_id", + "section_id", + "layout_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "item": { + "name": "item", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{\"json\": {}}')" + }, + "advanced_options": { + "name": "advanced_options", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{\"json\": {}}')" + } + }, + "indexes": {}, + "foreignKeys": { + "item_board_id_board_id_fk": { + "name": "item_board_id_board_id_fk", + "tableFrom": "item", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "item_id": { + "name": "item_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "layout": { + "name": "layout", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "column_count": { + "name": "column_count", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "breakpoint": { + "name": "breakpoint", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "layout_board_id_board_id_fk": { + "name": "layout_board_id_board_id_fk", + "tableFrom": "layout", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "layout_id": { + "name": "layout_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "media": { + "name": "media", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "LONGBLOB", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "media_creator_id_user_id_fk": { + "name": "media_creator_id_user_id_fk", + "tableFrom": "media", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "media_id": { + "name": "media_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "onboarding": { + "name": "onboarding", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step": { + "name": "step", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "previous_step": { + "name": "previous_step", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "onboarding_id": { + "name": "onboarding_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "search_engine": { + "name": "search_engine", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "short": { + "name": "short", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url_template": { + "name": "url_template", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'generic'" + }, + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "search_engine_integration_id_integration_id_fk": { + "name": "search_engine_integration_id_integration_id_fk", + "tableFrom": "search_engine", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "search_engine_id": { + "name": "search_engine_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "search_engine_short_unique": { + "name": "search_engine_short_unique", + "columns": [ + "short" + ] + } + }, + "checkConstraint": {} + }, + "section_collapse_state": { + "name": "section_collapse_state", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "collapsed": { + "name": "collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "section_collapse_state_user_id_user_id_fk": { + "name": "section_collapse_state_user_id_user_id_fk", + "tableFrom": "section_collapse_state", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_collapse_state_section_id_section_id_fk": { + "name": "section_collapse_state_section_id_section_id_fk", + "tableFrom": "section_collapse_state", + "tableTo": "section", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "section_collapse_state_user_id_section_id_pk": { + "name": "section_collapse_state_user_id_section_id_pk", + "columns": [ + "user_id", + "section_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "section_layout": { + "name": "section_layout", + "columns": { + "section_id": { + "name": "section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout_id": { + "name": "layout_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_section_id": { + "name": "parent_section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "x_offset": { + "name": "x_offset", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "y_offset": { + "name": "y_offset", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "section_layout_section_id_section_id_fk": { + "name": "section_layout_section_id_section_id_fk", + "tableFrom": "section_layout", + "tableTo": "section", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_layout_layout_id_layout_id_fk": { + "name": "section_layout_layout_id_layout_id_fk", + "tableFrom": "section_layout", + "tableTo": "layout", + "columnsFrom": [ + "layout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_layout_parent_section_id_section_id_fk": { + "name": "section_layout_parent_section_id_section_id_fk", + "tableFrom": "section_layout", + "tableTo": "section", + "columnsFrom": [ + "parent_section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "section_layout_section_id_layout_id_pk": { + "name": "section_layout_section_id_layout_id_pk", + "columns": [ + "section_id", + "layout_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "section": { + "name": "section", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "x_offset": { + "name": "x_offset", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "y_offset": { + "name": "y_offset", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('{\"json\": {}}')" + } + }, + "indexes": {}, + "foreignKeys": { + "section_board_id_board_id_fk": { + "name": "section_board_id_board_id_fk", + "tableFrom": "section", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "section_id": { + "name": "section_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "serverSetting": { + "name": "serverSetting", + "columns": { + "setting_key": { + "name": "setting_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{\"json\": {}}')" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "serverSetting_setting_key": { + "name": "serverSetting_setting_key", + "columns": [ + "setting_key" + ] + } + }, + "uniqueConstraints": { + "serverSetting_settingKey_unique": { + "name": "serverSetting_settingKey_unique", + "columns": [ + "setting_key" + ] + } + }, + "checkConstraint": {} + }, + "session": { + "name": "session", + "columns": { + "session_token": { + "name": "session_token", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_id_idx": { + "name": "user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_session_token": { + "name": "session_session_token", + "columns": [ + "session_token" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credentials'" + }, + "home_board_id": { + "name": "home_board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mobile_home_board_id": { + "name": "mobile_home_board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_search_engine_id": { + "name": "default_search_engine_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "open_search_in_new_tab": { + "name": "open_search_in_new_tab", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ddg_bangs": { + "name": "ddg_bangs", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "color_scheme": { + "name": "color_scheme", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'dark'" + }, + "first_day_of_week": { + "name": "first_day_of_week", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "ping_icons_enabled": { + "name": "ping_icons_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "completed_manage_tour": { + "name": "completed_manage_tour", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "completed_board_tour": { + "name": "completed_board_tour", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_home_board_id_board_id_fk": { + "name": "user_home_board_id_board_id_fk", + "tableFrom": "user", + "tableTo": "board", + "columnsFrom": [ + "home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_mobile_home_board_id_board_id_fk": { + "name": "user_mobile_home_board_id_board_id_fk", + "tableFrom": "user", + "tableTo": "board", + "columnsFrom": [ + "mobile_home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_default_search_engine_id_search_engine_id_fk": { + "name": "user_default_search_engine_id_search_engine_id_fk", + "tableFrom": "user", + "tableTo": "search_engine", + "columnsFrom": [ + "default_search_engine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_id": { + "name": "user_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trusted_certificate_hostname": { + "name": "trusted_certificate_hostname", + "columns": { + "hostname": { + "name": "hostname", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "thumbprint": { + "name": "thumbprint", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "certificate": { + "name": "certificate", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "trusted_certificate_hostname_hostname_thumbprint_pk": { + "name": "trusted_certificate_hostname_hostname_thumbprint_pk", + "columns": [ + "hostname", + "thumbprint" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "widget_secret": { + "name": "widget_secret", + "columns": { + "item_id": { + "name": "item_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "widget_secret_item_id_item_id_fk": { + "name": "widget_secret_item_id_item_id_fk", + "tableFrom": "widget_secret", + "tableTo": "item", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "widget_secret_item_id_kind_pk": { + "columns": [ + "item_id", + "kind" + ], + "name": "widget_secret_item_id_kind_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} diff --git a/packages/db/migrations/mysql/meta/_journal.json b/packages/db/migrations/mysql/meta/_journal.json index b2665bb91..4ce2bf038 100644 --- a/packages/db/migrations/mysql/meta/_journal.json +++ b/packages/db/migrations/mysql/meta/_journal.json @@ -295,6 +295,13 @@ "when": 1780764500000, "tag": "0041_merge_custom_widget_url", "breakpoints": true + }, + { + "idx": 42, + "version": "5", + "when": 1783176100870, + "tag": "0042_add_widget_secret", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/migrations/postgresql/0008_add_widget_secret.sql b/packages/db/migrations/postgresql/0008_add_widget_secret.sql new file mode 100644 index 000000000..e5423e774 --- /dev/null +++ b/packages/db/migrations/postgresql/0008_add_widget_secret.sql @@ -0,0 +1,9 @@ +CREATE TABLE "widget_secret" ( + "item_id" varchar(64) NOT NULL, + "kind" varchar(64) NOT NULL, + "value" text NOT NULL, + "updated_at" timestamp NOT NULL, + CONSTRAINT "widget_secret_item_id_kind_pk" PRIMARY KEY("item_id","kind") +); +--> statement-breakpoint +ALTER TABLE "widget_secret" ADD CONSTRAINT "widget_secret_item_id_item_id_fk" FOREIGN KEY ("item_id") REFERENCES "public"."item"("id") ON DELETE cascade ON UPDATE no action; diff --git a/packages/db/migrations/postgresql/meta/0007_snapshot.json b/packages/db/migrations/postgresql/meta/0007_snapshot.json new file mode 100644 index 000000000..4c4f167d7 --- /dev/null +++ b/packages/db/migrations/postgresql/meta/0007_snapshot.json @@ -0,0 +1,2413 @@ +{ + "id": "af9b8ec9-5d2e-470f-aed5-2d1e0792ae8f", + "prevId": "097e530f-04ee-4cf2-88a6-c68137f6319c", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "userId_idx": { + "name": "userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_provider_account_id_pk": { + "name": "account_provider_provider_account_id_pk", + "columns": [ + "provider", + "provider_account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apiKey": { + "name": "apiKey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "apiKey_user_id_user_id_fk": { + "name": "apiKey_user_id_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app": { + "name": "app", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "href": { + "name": "href", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ping_url": { + "name": "ping_url", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.boardGroupPermission": { + "name": "boardGroupPermission", + "schema": "", + "columns": { + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "boardGroupPermission_board_id_board_id_fk": { + "name": "boardGroupPermission_board_id_board_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardGroupPermission_group_id_group_id_fk": { + "name": "boardGroupPermission_group_id_group_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardGroupPermission_board_id_group_id_permission_pk": { + "name": "boardGroupPermission_board_id_group_id_permission_pk", + "columns": [ + "board_id", + "group_id", + "permission" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.boardUserPermission": { + "name": "boardUserPermission", + "schema": "", + "columns": { + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "boardUserPermission_board_id_board_id_fk": { + "name": "boardUserPermission_board_id_board_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardUserPermission_user_id_user_id_fk": { + "name": "boardUserPermission_user_id_user_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardUserPermission_board_id_user_id_permission_pk": { + "name": "boardUserPermission_board_id_user_id_permission_pk", + "columns": [ + "board_id", + "user_id", + "permission" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board": { + "name": "board", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "page_title": { + "name": "page_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "meta_title": { + "name": "meta_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_image_url": { + "name": "logo_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "favicon_image_url": { + "name": "favicon_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "background_image_url": { + "name": "background_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "background_image_attachment": { + "name": "background_image_attachment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "background_image_repeat": { + "name": "background_image_repeat", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'no-repeat'" + }, + "background_image_size": { + "name": "background_image_size", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'cover'" + }, + "primary_color": { + "name": "primary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#fa5252'" + }, + "secondary_color": { + "name": "secondary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#fd7e14'" + }, + "opacity": { + "name": "opacity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "custom_css": { + "name": "custom_css", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon_color": { + "name": "icon_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "item_radius": { + "name": "item_radius", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'lg'" + }, + "disable_status": { + "name": "disable_status", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "board_creator_id_user_id_fk": { + "name": "board_creator_id_user_id_fk", + "tableFrom": "board", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "board_name_unique": { + "name": "board_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cron_job_configuration": { + "name": "cron_job_configuration", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": true, + "notNull": true + }, + "cron_expression": { + "name": "cron_expression", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_widget_definition": { + "name": "custom_widget_definition", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "header_name": { + "name": "header_name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "method": { + "name": "method", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'GET'" + }, + "request_body": { + "name": "request_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_type": { + "name": "display_type", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "default": "'singleValue'" + }, + "display_config": { + "name": "display_config", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{\"json\": {}}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "custom_widget_definition_creator_id_user_id_fk": { + "name": "custom_widget_definition_creator_id_user_id_fk", + "tableFrom": "custom_widget_definition", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_widget_secret": { + "name": "custom_widget_secret", + "schema": "", + "columns": { + "kind": { + "name": "kind", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "definition_id": { + "name": "definition_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "custom_widget_secret_definition_id_custom_widget_definition_id_fk": { + "name": "custom_widget_secret_definition_id_custom_widget_definition_id_fk", + "tableFrom": "custom_widget_secret", + "tableTo": "custom_widget_definition", + "columnsFrom": [ + "definition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "custom_widget_secret_definition_id_kind_pk": { + "name": "custom_widget_secret_definition_id_kind_pk", + "columns": [ + "definition_id", + "kind" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.groupMember": { + "name": "groupMember", + "schema": "", + "columns": { + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "groupMember_group_id_group_id_fk": { + "name": "groupMember_group_id_group_id_fk", + "tableFrom": "groupMember", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "groupMember_user_id_user_id_fk": { + "name": "groupMember_user_id_user_id_fk", + "tableFrom": "groupMember", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "groupMember_group_id_user_id_pk": { + "name": "groupMember_group_id_user_id_pk", + "columns": [ + "group_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.groupPermission": { + "name": "groupPermission", + "schema": "", + "columns": { + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "groupPermission_group_id_group_id_fk": { + "name": "groupPermission_group_id_group_id_fk", + "tableFrom": "groupPermission", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.group": { + "name": "group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "home_board_id": { + "name": "home_board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "mobile_home_board_id": { + "name": "mobile_home_board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "smallint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "group_owner_id_user_id_fk": { + "name": "group_owner_id_user_id_fk", + "tableFrom": "group", + "tableTo": "user", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "group_home_board_id_board_id_fk": { + "name": "group_home_board_id_board_id_fk", + "tableFrom": "group", + "tableTo": "board", + "columnsFrom": [ + "home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "group_mobile_home_board_id_board_id_fk": { + "name": "group_mobile_home_board_id_board_id_fk", + "tableFrom": "group", + "tableTo": "board", + "columnsFrom": [ + "mobile_home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "group_name_unique": { + "name": "group_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.iconRepository": { + "name": "iconRepository", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(150)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.icon": { + "name": "icon", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(250)", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon_repository_id": { + "name": "icon_repository_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "icon_icon_repository_id_iconRepository_id_fk": { + "name": "icon_icon_repository_id_iconRepository_id_fk", + "tableFrom": "icon", + "tableTo": "iconRepository", + "columnsFrom": [ + "icon_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integrationGroupPermissions": { + "name": "integrationGroupPermissions", + "schema": "", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "integrationGroupPermissions_integration_id_integration_id_fk": { + "name": "integrationGroupPermissions_integration_id_integration_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationGroupPermissions_group_id_group_id_fk": { + "name": "integrationGroupPermissions_group_id_group_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_group_permission__pk": { + "name": "integration_group_permission__pk", + "columns": [ + "integration_id", + "group_id", + "permission" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_item": { + "name": "integration_item", + "schema": "", + "columns": { + "item_id": { + "name": "item_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "integration_item_item_id_item_id_fk": { + "name": "integration_item_item_id_item_id_fk", + "tableFrom": "integration_item", + "tableTo": "item", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integration_item_integration_id_integration_id_fk": { + "name": "integration_item_integration_id_integration_id_fk", + "tableFrom": "integration_item", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_item_item_id_integration_id_pk": { + "name": "integration_item_item_id_integration_id_pk", + "columns": [ + "item_id", + "integration_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integrationSecret": { + "name": "integrationSecret", + "schema": "", + "columns": { + "kind": { + "name": "kind", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "integration_secret__kind_idx": { + "name": "integration_secret__kind_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_secret__updated_at_idx": { + "name": "integration_secret__updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integrationSecret_integration_id_integration_id_fk": { + "name": "integrationSecret_integration_id_integration_id_fk", + "tableFrom": "integrationSecret", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationSecret_integration_id_kind_pk": { + "name": "integrationSecret_integration_id_kind_pk", + "columns": [ + "integration_id", + "kind" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integrationUserPermission": { + "name": "integrationUserPermission", + "schema": "", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "integrationUserPermission_integration_id_integration_id_fk": { + "name": "integrationUserPermission_integration_id_integration_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationUserPermission_user_id_user_id_fk": { + "name": "integrationUserPermission_user_id_user_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationUserPermission_integration_id_user_id_permission_pk": { + "name": "integrationUserPermission_integration_id_user_id_permission_pk", + "columns": [ + "integration_id", + "user_id", + "permission" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration": { + "name": "integration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "app_id": { + "name": "app_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "integration__kind_idx": { + "name": "integration__kind_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_app_id_app_id_fk": { + "name": "integration_app_id_app_id_fk", + "tableFrom": "integration", + "tableTo": "app", + "columnsFrom": [ + "app_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invite": { + "name": "invite", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true + }, + "expiration_date": { + "name": "expiration_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "invite_creator_id_user_id_fk": { + "name": "invite_creator_id_user_id_fk", + "tableFrom": "invite", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invite_token_unique": { + "name": "invite_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.item_layout": { + "name": "item_layout", + "schema": "", + "columns": { + "item_id": { + "name": "item_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "layout_id": { + "name": "layout_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "x_offset": { + "name": "x_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "y_offset": { + "name": "y_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "item_layout_item_id_item_id_fk": { + "name": "item_layout_item_id_item_id_fk", + "tableFrom": "item_layout", + "tableTo": "item", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "item_layout_section_id_section_id_fk": { + "name": "item_layout_section_id_section_id_fk", + "tableFrom": "item_layout", + "tableTo": "section", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "item_layout_layout_id_layout_id_fk": { + "name": "item_layout_layout_id_layout_id_fk", + "tableFrom": "item_layout", + "tableTo": "layout", + "columnsFrom": [ + "layout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "item_layout_item_id_section_id_layout_id_pk": { + "name": "item_layout_item_id_section_id_layout_id_pk", + "columns": [ + "item_id", + "section_id", + "layout_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.item": { + "name": "item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{\"json\": {}}'" + }, + "advanced_options": { + "name": "advanced_options", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{\"json\": {}}'" + } + }, + "indexes": {}, + "foreignKeys": { + "item_board_id_board_id_fk": { + "name": "item_board_id_board_id_fk", + "tableFrom": "item", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.layout": { + "name": "layout", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "column_count": { + "name": "column_count", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "breakpoint": { + "name": "breakpoint", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "layout_board_id_board_id_fk": { + "name": "layout_board_id_board_id_fk", + "tableFrom": "layout", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "media_creator_id_user_id_fk": { + "name": "media_creator_id_user_id_fk", + "tableFrom": "media", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.onboarding": { + "name": "onboarding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "step": { + "name": "step", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "previous_step": { + "name": "previous_step", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.search_engine": { + "name": "search_engine", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "short": { + "name": "short", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url_template": { + "name": "url_template", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "default": "'generic'" + }, + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "search_engine_integration_id_integration_id_fk": { + "name": "search_engine_integration_id_integration_id_fk", + "tableFrom": "search_engine", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "search_engine_short_unique": { + "name": "search_engine_short_unique", + "nullsNotDistinct": false, + "columns": [ + "short" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.section_collapse_state": { + "name": "section_collapse_state", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "collapsed": { + "name": "collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "section_collapse_state_user_id_user_id_fk": { + "name": "section_collapse_state_user_id_user_id_fk", + "tableFrom": "section_collapse_state", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_collapse_state_section_id_section_id_fk": { + "name": "section_collapse_state_section_id_section_id_fk", + "tableFrom": "section_collapse_state", + "tableTo": "section", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "section_collapse_state_user_id_section_id_pk": { + "name": "section_collapse_state_user_id_section_id_pk", + "columns": [ + "user_id", + "section_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.section_layout": { + "name": "section_layout", + "schema": "", + "columns": { + "section_id": { + "name": "section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "layout_id": { + "name": "layout_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "parent_section_id": { + "name": "parent_section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "x_offset": { + "name": "x_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "y_offset": { + "name": "y_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "section_layout_section_id_section_id_fk": { + "name": "section_layout_section_id_section_id_fk", + "tableFrom": "section_layout", + "tableTo": "section", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_layout_layout_id_layout_id_fk": { + "name": "section_layout_layout_id_layout_id_fk", + "tableFrom": "section_layout", + "tableTo": "layout", + "columnsFrom": [ + "layout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_layout_parent_section_id_section_id_fk": { + "name": "section_layout_parent_section_id_section_id_fk", + "tableFrom": "section_layout", + "tableTo": "section", + "columnsFrom": [ + "parent_section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "section_layout_section_id_layout_id_pk": { + "name": "section_layout_section_id_layout_id_pk", + "columns": [ + "section_id", + "layout_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.section": { + "name": "section", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "x_offset": { + "name": "x_offset", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "y_offset": { + "name": "y_offset", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'{\"json\": {}}'" + } + }, + "indexes": {}, + "foreignKeys": { + "section_board_id_board_id_fk": { + "name": "section_board_id_board_id_fk", + "tableFrom": "section", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.serverSetting": { + "name": "serverSetting", + "schema": "", + "columns": { + "setting_key": { + "name": "setting_key", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{\"json\": {}}'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "serverSetting_settingKey_unique": { + "name": "serverSetting_settingKey_unique", + "nullsNotDistinct": false, + "columns": [ + "setting_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "session_token": { + "name": "session_token", + "type": "varchar(512)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "user_id_idx": { + "name": "user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "default": "'credentials'" + }, + "home_board_id": { + "name": "home_board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "mobile_home_board_id": { + "name": "mobile_home_board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "default_search_engine_id": { + "name": "default_search_engine_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "open_search_in_new_tab": { + "name": "open_search_in_new_tab", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ddg_bangs": { + "name": "ddg_bangs", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "color_scheme": { + "name": "color_scheme", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'dark'" + }, + "first_day_of_week": { + "name": "first_day_of_week", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "ping_icons_enabled": { + "name": "ping_icons_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "completed_manage_tour": { + "name": "completed_manage_tour", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "completed_board_tour": { + "name": "completed_board_tour", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_home_board_id_board_id_fk": { + "name": "user_home_board_id_board_id_fk", + "tableFrom": "user", + "tableTo": "board", + "columnsFrom": [ + "home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_mobile_home_board_id_board_id_fk": { + "name": "user_mobile_home_board_id_board_id_fk", + "tableFrom": "user", + "tableTo": "board", + "columnsFrom": [ + "mobile_home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_default_search_engine_id_search_engine_id_fk": { + "name": "user_default_search_engine_id_search_engine_id_fk", + "tableFrom": "user", + "tableTo": "search_engine", + "columnsFrom": [ + "default_search_engine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verificationToken": { + "name": "verificationToken", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_certificate_hostname": { + "name": "trusted_certificate_hostname", + "schema": "", + "columns": { + "hostname": { + "name": "hostname", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "thumbprint": { + "name": "thumbprint", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "certificate": { + "name": "certificate", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "trusted_certificate_hostname_hostname_thumbprint_pk": { + "name": "trusted_certificate_hostname_hostname_thumbprint_pk", + "columns": [ + "hostname", + "thumbprint" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/postgresql/meta/0008_snapshot.json b/packages/db/migrations/postgresql/meta/0008_snapshot.json new file mode 100644 index 000000000..ab3303ac9 --- /dev/null +++ b/packages/db/migrations/postgresql/meta/0008_snapshot.json @@ -0,0 +1,2471 @@ +{ + "id": "39cf38d0-0503-4c79-b7f4-632fa062fa2a", + "prevId": "af9b8ec9-5d2e-470f-aed5-2d1e0792ae8f", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "userId_idx": { + "name": "userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_provider_account_id_pk": { + "name": "account_provider_provider_account_id_pk", + "columns": [ + "provider", + "provider_account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apiKey": { + "name": "apiKey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "apiKey_user_id_user_id_fk": { + "name": "apiKey_user_id_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app": { + "name": "app", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "href": { + "name": "href", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ping_url": { + "name": "ping_url", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.boardGroupPermission": { + "name": "boardGroupPermission", + "schema": "", + "columns": { + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "boardGroupPermission_board_id_board_id_fk": { + "name": "boardGroupPermission_board_id_board_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardGroupPermission_group_id_group_id_fk": { + "name": "boardGroupPermission_group_id_group_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardGroupPermission_board_id_group_id_permission_pk": { + "name": "boardGroupPermission_board_id_group_id_permission_pk", + "columns": [ + "board_id", + "group_id", + "permission" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.boardUserPermission": { + "name": "boardUserPermission", + "schema": "", + "columns": { + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "boardUserPermission_board_id_board_id_fk": { + "name": "boardUserPermission_board_id_board_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardUserPermission_user_id_user_id_fk": { + "name": "boardUserPermission_user_id_user_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardUserPermission_board_id_user_id_permission_pk": { + "name": "boardUserPermission_board_id_user_id_permission_pk", + "columns": [ + "board_id", + "user_id", + "permission" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board": { + "name": "board", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "page_title": { + "name": "page_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "meta_title": { + "name": "meta_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_image_url": { + "name": "logo_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "favicon_image_url": { + "name": "favicon_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "background_image_url": { + "name": "background_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "background_image_attachment": { + "name": "background_image_attachment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "background_image_repeat": { + "name": "background_image_repeat", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'no-repeat'" + }, + "background_image_size": { + "name": "background_image_size", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'cover'" + }, + "primary_color": { + "name": "primary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#fa5252'" + }, + "secondary_color": { + "name": "secondary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#fd7e14'" + }, + "opacity": { + "name": "opacity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "custom_css": { + "name": "custom_css", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon_color": { + "name": "icon_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "item_radius": { + "name": "item_radius", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'lg'" + }, + "disable_status": { + "name": "disable_status", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "board_creator_id_user_id_fk": { + "name": "board_creator_id_user_id_fk", + "tableFrom": "board", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "board_name_unique": { + "name": "board_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cron_job_configuration": { + "name": "cron_job_configuration", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": true, + "notNull": true + }, + "cron_expression": { + "name": "cron_expression", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_widget_definition": { + "name": "custom_widget_definition", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "header_name": { + "name": "header_name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "method": { + "name": "method", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'GET'" + }, + "request_body": { + "name": "request_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_type": { + "name": "display_type", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "default": "'singleValue'" + }, + "display_config": { + "name": "display_config", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{\"json\": {}}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "custom_widget_definition_creator_id_user_id_fk": { + "name": "custom_widget_definition_creator_id_user_id_fk", + "tableFrom": "custom_widget_definition", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_widget_secret": { + "name": "custom_widget_secret", + "schema": "", + "columns": { + "kind": { + "name": "kind", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "definition_id": { + "name": "definition_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "custom_widget_secret_definition_id_custom_widget_definition_id_fk": { + "name": "custom_widget_secret_definition_id_custom_widget_definition_id_fk", + "tableFrom": "custom_widget_secret", + "tableTo": "custom_widget_definition", + "columnsFrom": [ + "definition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "custom_widget_secret_definition_id_kind_pk": { + "name": "custom_widget_secret_definition_id_kind_pk", + "columns": [ + "definition_id", + "kind" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.groupMember": { + "name": "groupMember", + "schema": "", + "columns": { + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "groupMember_group_id_group_id_fk": { + "name": "groupMember_group_id_group_id_fk", + "tableFrom": "groupMember", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "groupMember_user_id_user_id_fk": { + "name": "groupMember_user_id_user_id_fk", + "tableFrom": "groupMember", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "groupMember_group_id_user_id_pk": { + "name": "groupMember_group_id_user_id_pk", + "columns": [ + "group_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.groupPermission": { + "name": "groupPermission", + "schema": "", + "columns": { + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "groupPermission_group_id_group_id_fk": { + "name": "groupPermission_group_id_group_id_fk", + "tableFrom": "groupPermission", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.group": { + "name": "group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "home_board_id": { + "name": "home_board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "mobile_home_board_id": { + "name": "mobile_home_board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "smallint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "group_owner_id_user_id_fk": { + "name": "group_owner_id_user_id_fk", + "tableFrom": "group", + "tableTo": "user", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "group_home_board_id_board_id_fk": { + "name": "group_home_board_id_board_id_fk", + "tableFrom": "group", + "tableTo": "board", + "columnsFrom": [ + "home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "group_mobile_home_board_id_board_id_fk": { + "name": "group_mobile_home_board_id_board_id_fk", + "tableFrom": "group", + "tableTo": "board", + "columnsFrom": [ + "mobile_home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "group_name_unique": { + "name": "group_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.iconRepository": { + "name": "iconRepository", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(150)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.icon": { + "name": "icon", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(250)", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon_repository_id": { + "name": "icon_repository_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "icon_icon_repository_id_iconRepository_id_fk": { + "name": "icon_icon_repository_id_iconRepository_id_fk", + "tableFrom": "icon", + "tableTo": "iconRepository", + "columnsFrom": [ + "icon_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integrationGroupPermissions": { + "name": "integrationGroupPermissions", + "schema": "", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "integrationGroupPermissions_integration_id_integration_id_fk": { + "name": "integrationGroupPermissions_integration_id_integration_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationGroupPermissions_group_id_group_id_fk": { + "name": "integrationGroupPermissions_group_id_group_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_group_permission__pk": { + "name": "integration_group_permission__pk", + "columns": [ + "integration_id", + "group_id", + "permission" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_item": { + "name": "integration_item", + "schema": "", + "columns": { + "item_id": { + "name": "item_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "integration_item_item_id_item_id_fk": { + "name": "integration_item_item_id_item_id_fk", + "tableFrom": "integration_item", + "tableTo": "item", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integration_item_integration_id_integration_id_fk": { + "name": "integration_item_integration_id_integration_id_fk", + "tableFrom": "integration_item", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_item_item_id_integration_id_pk": { + "name": "integration_item_item_id_integration_id_pk", + "columns": [ + "item_id", + "integration_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integrationSecret": { + "name": "integrationSecret", + "schema": "", + "columns": { + "kind": { + "name": "kind", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "integration_secret__kind_idx": { + "name": "integration_secret__kind_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_secret__updated_at_idx": { + "name": "integration_secret__updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integrationSecret_integration_id_integration_id_fk": { + "name": "integrationSecret_integration_id_integration_id_fk", + "tableFrom": "integrationSecret", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationSecret_integration_id_kind_pk": { + "name": "integrationSecret_integration_id_kind_pk", + "columns": [ + "integration_id", + "kind" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integrationUserPermission": { + "name": "integrationUserPermission", + "schema": "", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "integrationUserPermission_integration_id_integration_id_fk": { + "name": "integrationUserPermission_integration_id_integration_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationUserPermission_user_id_user_id_fk": { + "name": "integrationUserPermission_user_id_user_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationUserPermission_integration_id_user_id_permission_pk": { + "name": "integrationUserPermission_integration_id_user_id_permission_pk", + "columns": [ + "integration_id", + "user_id", + "permission" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration": { + "name": "integration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "app_id": { + "name": "app_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "integration__kind_idx": { + "name": "integration__kind_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_app_id_app_id_fk": { + "name": "integration_app_id_app_id_fk", + "tableFrom": "integration", + "tableTo": "app", + "columnsFrom": [ + "app_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invite": { + "name": "invite", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true + }, + "expiration_date": { + "name": "expiration_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "invite_creator_id_user_id_fk": { + "name": "invite_creator_id_user_id_fk", + "tableFrom": "invite", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invite_token_unique": { + "name": "invite_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.item_layout": { + "name": "item_layout", + "schema": "", + "columns": { + "item_id": { + "name": "item_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "layout_id": { + "name": "layout_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "x_offset": { + "name": "x_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "y_offset": { + "name": "y_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "item_layout_item_id_item_id_fk": { + "name": "item_layout_item_id_item_id_fk", + "tableFrom": "item_layout", + "tableTo": "item", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "item_layout_section_id_section_id_fk": { + "name": "item_layout_section_id_section_id_fk", + "tableFrom": "item_layout", + "tableTo": "section", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "item_layout_layout_id_layout_id_fk": { + "name": "item_layout_layout_id_layout_id_fk", + "tableFrom": "item_layout", + "tableTo": "layout", + "columnsFrom": [ + "layout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "item_layout_item_id_section_id_layout_id_pk": { + "name": "item_layout_item_id_section_id_layout_id_pk", + "columns": [ + "item_id", + "section_id", + "layout_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.item": { + "name": "item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{\"json\": {}}'" + }, + "advanced_options": { + "name": "advanced_options", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{\"json\": {}}'" + } + }, + "indexes": {}, + "foreignKeys": { + "item_board_id_board_id_fk": { + "name": "item_board_id_board_id_fk", + "tableFrom": "item", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.layout": { + "name": "layout", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "column_count": { + "name": "column_count", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "breakpoint": { + "name": "breakpoint", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "layout_board_id_board_id_fk": { + "name": "layout_board_id_board_id_fk", + "tableFrom": "layout", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "media_creator_id_user_id_fk": { + "name": "media_creator_id_user_id_fk", + "tableFrom": "media", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.onboarding": { + "name": "onboarding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "step": { + "name": "step", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "previous_step": { + "name": "previous_step", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.search_engine": { + "name": "search_engine", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "short": { + "name": "short", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url_template": { + "name": "url_template", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "default": "'generic'" + }, + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "search_engine_integration_id_integration_id_fk": { + "name": "search_engine_integration_id_integration_id_fk", + "tableFrom": "search_engine", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "search_engine_short_unique": { + "name": "search_engine_short_unique", + "nullsNotDistinct": false, + "columns": [ + "short" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.section_collapse_state": { + "name": "section_collapse_state", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "collapsed": { + "name": "collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "section_collapse_state_user_id_user_id_fk": { + "name": "section_collapse_state_user_id_user_id_fk", + "tableFrom": "section_collapse_state", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_collapse_state_section_id_section_id_fk": { + "name": "section_collapse_state_section_id_section_id_fk", + "tableFrom": "section_collapse_state", + "tableTo": "section", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "section_collapse_state_user_id_section_id_pk": { + "name": "section_collapse_state_user_id_section_id_pk", + "columns": [ + "user_id", + "section_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.section_layout": { + "name": "section_layout", + "schema": "", + "columns": { + "section_id": { + "name": "section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "layout_id": { + "name": "layout_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "parent_section_id": { + "name": "parent_section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "x_offset": { + "name": "x_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "y_offset": { + "name": "y_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "section_layout_section_id_section_id_fk": { + "name": "section_layout_section_id_section_id_fk", + "tableFrom": "section_layout", + "tableTo": "section", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_layout_layout_id_layout_id_fk": { + "name": "section_layout_layout_id_layout_id_fk", + "tableFrom": "section_layout", + "tableTo": "layout", + "columnsFrom": [ + "layout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_layout_parent_section_id_section_id_fk": { + "name": "section_layout_parent_section_id_section_id_fk", + "tableFrom": "section_layout", + "tableTo": "section", + "columnsFrom": [ + "parent_section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "section_layout_section_id_layout_id_pk": { + "name": "section_layout_section_id_layout_id_pk", + "columns": [ + "section_id", + "layout_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.section": { + "name": "section", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "x_offset": { + "name": "x_offset", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "y_offset": { + "name": "y_offset", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'{\"json\": {}}'" + } + }, + "indexes": {}, + "foreignKeys": { + "section_board_id_board_id_fk": { + "name": "section_board_id_board_id_fk", + "tableFrom": "section", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.serverSetting": { + "name": "serverSetting", + "schema": "", + "columns": { + "setting_key": { + "name": "setting_key", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{\"json\": {}}'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "serverSetting_settingKey_unique": { + "name": "serverSetting_settingKey_unique", + "nullsNotDistinct": false, + "columns": [ + "setting_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "session_token": { + "name": "session_token", + "type": "varchar(512)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "user_id_idx": { + "name": "user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "default": "'credentials'" + }, + "home_board_id": { + "name": "home_board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "mobile_home_board_id": { + "name": "mobile_home_board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "default_search_engine_id": { + "name": "default_search_engine_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "open_search_in_new_tab": { + "name": "open_search_in_new_tab", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ddg_bangs": { + "name": "ddg_bangs", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "color_scheme": { + "name": "color_scheme", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'dark'" + }, + "first_day_of_week": { + "name": "first_day_of_week", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "ping_icons_enabled": { + "name": "ping_icons_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "completed_manage_tour": { + "name": "completed_manage_tour", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "completed_board_tour": { + "name": "completed_board_tour", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_home_board_id_board_id_fk": { + "name": "user_home_board_id_board_id_fk", + "tableFrom": "user", + "tableTo": "board", + "columnsFrom": [ + "home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_mobile_home_board_id_board_id_fk": { + "name": "user_mobile_home_board_id_board_id_fk", + "tableFrom": "user", + "tableTo": "board", + "columnsFrom": [ + "mobile_home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_default_search_engine_id_search_engine_id_fk": { + "name": "user_default_search_engine_id_search_engine_id_fk", + "tableFrom": "user", + "tableTo": "search_engine", + "columnsFrom": [ + "default_search_engine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verificationToken": { + "name": "verificationToken", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_certificate_hostname": { + "name": "trusted_certificate_hostname", + "schema": "", + "columns": { + "hostname": { + "name": "hostname", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "thumbprint": { + "name": "thumbprint", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "certificate": { + "name": "certificate", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "trusted_certificate_hostname_hostname_thumbprint_pk": { + "name": "trusted_certificate_hostname_hostname_thumbprint_pk", + "columns": [ + "hostname", + "thumbprint" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.widget_secret": { + "name": "widget_secret", + "schema": "public", + "columns": { + "item_id": { + "name": "item_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "widget_secret_item_id_item_id_fk": { + "name": "widget_secret_item_id_item_id_fk", + "tableFrom": "widget_secret", + "tableTo": "item", + "schemaTo": "public", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "widget_secret_item_id_kind_pk": { + "columns": [ + "item_id", + "kind" + ], + "name": "widget_secret_item_id_kind_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/postgresql/meta/_journal.json b/packages/db/migrations/postgresql/meta/_journal.json index 495b5e2fb..829168b9e 100644 --- a/packages/db/migrations/postgresql/meta/_journal.json +++ b/packages/db/migrations/postgresql/meta/_journal.json @@ -57,6 +57,13 @@ "when": 1780764500000, "tag": "0007_merge_custom_widget_url", "breakpoints": true + }, + { + "idx": 8, + "version": "7", + "when": 1783176100870, + "tag": "0008_add_widget_secret", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/migrations/sqlite/0040_add_widget_secret.sql b/packages/db/migrations/sqlite/0040_add_widget_secret.sql new file mode 100644 index 000000000..06caa5ed4 --- /dev/null +++ b/packages/db/migrations/sqlite/0040_add_widget_secret.sql @@ -0,0 +1,8 @@ +CREATE TABLE `widget_secret` ( + `item_id` text NOT NULL, + `kind` text NOT NULL, + `value` text NOT NULL, + `updated_at` integer NOT NULL, + PRIMARY KEY(`item_id`, `kind`), + FOREIGN KEY (`item_id`) REFERENCES `item`(`id`) ON UPDATE no action ON DELETE cascade +); diff --git a/packages/db/migrations/sqlite/0040_charming_tag.sql b/packages/db/migrations/sqlite/0040_charming_tag.sql deleted file mode 100644 index 2fa64f42d..000000000 --- a/packages/db/migrations/sqlite/0040_charming_tag.sql +++ /dev/null @@ -1,3 +0,0 @@ --- Migration 0040: changes already applied by 0039_merge_custom_widget_url --- base_url renamed to url, endpoint dropped, enabled added via table recreation in 0039 -SELECT 1; diff --git a/packages/db/migrations/sqlite/0041_adorable_marten_broadcloak.sql b/packages/db/migrations/sqlite/0041_adorable_marten_broadcloak.sql deleted file mode 100644 index 9dfe78a88..000000000 --- a/packages/db/migrations/sqlite/0041_adorable_marten_broadcloak.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `integration` ADD `creator_id` text REFERENCES user(id); \ No newline at end of file diff --git a/packages/db/migrations/sqlite/meta/0039_snapshot.json b/packages/db/migrations/sqlite/meta/0039_snapshot.json new file mode 100644 index 000000000..2b5d2cf26 --- /dev/null +++ b/packages/db/migrations/sqlite/meta/0039_snapshot.json @@ -0,0 +1,2455 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "661fcf56-12b6-48a9-8d70-24032f0ec338", + "prevId": "78c75619-4e39-4774-9f91-573f629144d0", + "tables": { + "account": { + "name": "account", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "userId_idx": { + "name": "userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_provider_account_id_pk": { + "columns": [ + "provider", + "provider_account_id" + ], + "name": "account_provider_provider_account_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "apiKey": { + "name": "apiKey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "apiKey_user_id_user_id_fk": { + "name": "apiKey_user_id_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app": { + "name": "app", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "href": { + "name": "href", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ping_url": { + "name": "ping_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "boardGroupPermission": { + "name": "boardGroupPermission", + "columns": { + "board_id": { + "name": "board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "boardGroupPermission_board_id_board_id_fk": { + "name": "boardGroupPermission_board_id_board_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardGroupPermission_group_id_group_id_fk": { + "name": "boardGroupPermission_group_id_group_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardGroupPermission_board_id_group_id_permission_pk": { + "columns": [ + "board_id", + "group_id", + "permission" + ], + "name": "boardGroupPermission_board_id_group_id_permission_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "boardUserPermission": { + "name": "boardUserPermission", + "columns": { + "board_id": { + "name": "board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "boardUserPermission_board_id_board_id_fk": { + "name": "boardUserPermission_board_id_board_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardUserPermission_user_id_user_id_fk": { + "name": "boardUserPermission_user_id_user_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardUserPermission_board_id_user_id_permission_pk": { + "columns": [ + "board_id", + "user_id", + "permission" + ], + "name": "boardUserPermission_board_id_user_id_permission_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "board": { + "name": "board", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_public": { + "name": "is_public", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "page_title": { + "name": "page_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "meta_title": { + "name": "meta_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logo_image_url": { + "name": "logo_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "favicon_image_url": { + "name": "favicon_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "background_image_url": { + "name": "background_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "background_image_attachment": { + "name": "background_image_attachment", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'fixed'" + }, + "background_image_repeat": { + "name": "background_image_repeat", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'no-repeat'" + }, + "background_image_size": { + "name": "background_image_size", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'cover'" + }, + "primary_color": { + "name": "primary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'#fa5252'" + }, + "secondary_color": { + "name": "secondary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'#fd7e14'" + }, + "opacity": { + "name": "opacity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 100 + }, + "custom_css": { + "name": "custom_css", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon_color": { + "name": "icon_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "item_radius": { + "name": "item_radius", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'lg'" + }, + "disable_status": { + "name": "disable_status", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "board_name_unique": { + "name": "board_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "board_creator_id_user_id_fk": { + "name": "board_creator_id_user_id_fk", + "tableFrom": "board", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "cron_job_configuration": { + "name": "cron_job_configuration", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "custom_widget_definition": { + "name": "custom_widget_definition", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "header_name": { + "name": "header_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'GET'" + }, + "request_body": { + "name": "request_body", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_type": { + "name": "display_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'singleValue'" + }, + "display_config": { + "name": "display_config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{\"json\": {}}'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "custom_widget_definition_creator_id_user_id_fk": { + "name": "custom_widget_definition_creator_id_user_id_fk", + "tableFrom": "custom_widget_definition", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "custom_widget_secret": { + "name": "custom_widget_secret", + "columns": { + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "definition_id": { + "name": "definition_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "custom_widget_secret_definition_id_custom_widget_definition_id_fk": { + "name": "custom_widget_secret_definition_id_custom_widget_definition_id_fk", + "tableFrom": "custom_widget_secret", + "tableTo": "custom_widget_definition", + "columnsFrom": [ + "definition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "custom_widget_secret_definition_id_kind_pk": { + "columns": [ + "definition_id", + "kind" + ], + "name": "custom_widget_secret_definition_id_kind_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "groupMember": { + "name": "groupMember", + "columns": { + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "groupMember_group_id_group_id_fk": { + "name": "groupMember_group_id_group_id_fk", + "tableFrom": "groupMember", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "groupMember_user_id_user_id_fk": { + "name": "groupMember_user_id_user_id_fk", + "tableFrom": "groupMember", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "groupMember_group_id_user_id_pk": { + "columns": [ + "group_id", + "user_id" + ], + "name": "groupMember_group_id_user_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "groupPermission": { + "name": "groupPermission", + "columns": { + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "groupPermission_group_id_group_id_fk": { + "name": "groupPermission_group_id_group_id_fk", + "tableFrom": "groupPermission", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "group": { + "name": "group", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "home_board_id": { + "name": "home_board_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mobile_home_board_id": { + "name": "mobile_home_board_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "group_name_unique": { + "name": "group_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "group_owner_id_user_id_fk": { + "name": "group_owner_id_user_id_fk", + "tableFrom": "group", + "tableTo": "user", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "group_home_board_id_board_id_fk": { + "name": "group_home_board_id_board_id_fk", + "tableFrom": "group", + "tableTo": "board", + "columnsFrom": [ + "home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "group_mobile_home_board_id_board_id_fk": { + "name": "group_mobile_home_board_id_board_id_fk", + "tableFrom": "group", + "tableTo": "board", + "columnsFrom": [ + "mobile_home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "iconRepository": { + "name": "iconRepository", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "icon": { + "name": "icon", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon_repository_id": { + "name": "icon_repository_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "icon_icon_repository_id_iconRepository_id_fk": { + "name": "icon_icon_repository_id_iconRepository_id_fk", + "tableFrom": "icon", + "tableTo": "iconRepository", + "columnsFrom": [ + "icon_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integrationGroupPermissions": { + "name": "integrationGroupPermissions", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integrationGroupPermissions_integration_id_integration_id_fk": { + "name": "integrationGroupPermissions_integration_id_integration_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationGroupPermissions_group_id_group_id_fk": { + "name": "integrationGroupPermissions_group_id_group_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationGroupPermissions_integration_id_group_id_permission_pk": { + "columns": [ + "integration_id", + "group_id", + "permission" + ], + "name": "integrationGroupPermissions_integration_id_group_id_permission_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integration_item": { + "name": "integration_item", + "columns": { + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integration_item_item_id_item_id_fk": { + "name": "integration_item_item_id_item_id_fk", + "tableFrom": "integration_item", + "tableTo": "item", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integration_item_integration_id_integration_id_fk": { + "name": "integration_item_integration_id_integration_id_fk", + "tableFrom": "integration_item", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_item_item_id_integration_id_pk": { + "columns": [ + "item_id", + "integration_id" + ], + "name": "integration_item_item_id_integration_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integrationSecret": { + "name": "integrationSecret", + "columns": { + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "integration_secret__kind_idx": { + "name": "integration_secret__kind_idx", + "columns": [ + "kind" + ], + "isUnique": false + }, + "integration_secret__updated_at_idx": { + "name": "integration_secret__updated_at_idx", + "columns": [ + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "integrationSecret_integration_id_integration_id_fk": { + "name": "integrationSecret_integration_id_integration_id_fk", + "tableFrom": "integrationSecret", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationSecret_integration_id_kind_pk": { + "columns": [ + "integration_id", + "kind" + ], + "name": "integrationSecret_integration_id_kind_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integrationUserPermission": { + "name": "integrationUserPermission", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integrationUserPermission_integration_id_integration_id_fk": { + "name": "integrationUserPermission_integration_id_integration_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationUserPermission_user_id_user_id_fk": { + "name": "integrationUserPermission_user_id_user_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationUserPermission_integration_id_user_id_permission_pk": { + "columns": [ + "integration_id", + "user_id", + "permission" + ], + "name": "integrationUserPermission_integration_id_user_id_permission_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integration": { + "name": "integration", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "integration__kind_idx": { + "name": "integration__kind_idx", + "columns": [ + "kind" + ], + "isUnique": false + } + }, + "foreignKeys": { + "integration_app_id_app_id_fk": { + "name": "integration_app_id_app_id_fk", + "tableFrom": "integration", + "tableTo": "app", + "columnsFrom": [ + "app_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invite": { + "name": "invite", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expiration_date": { + "name": "expiration_date", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invite_token_unique": { + "name": "invite_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "invite_creator_id_user_id_fk": { + "name": "invite_creator_id_user_id_fk", + "tableFrom": "invite", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "item_layout": { + "name": "item_layout", + "columns": { + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout_id": { + "name": "layout_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "x_offset": { + "name": "x_offset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "y_offset": { + "name": "y_offset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "item_layout_item_id_item_id_fk": { + "name": "item_layout_item_id_item_id_fk", + "tableFrom": "item_layout", + "tableTo": "item", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "item_layout_section_id_section_id_fk": { + "name": "item_layout_section_id_section_id_fk", + "tableFrom": "item_layout", + "tableTo": "section", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "item_layout_layout_id_layout_id_fk": { + "name": "item_layout_layout_id_layout_id_fk", + "tableFrom": "item_layout", + "tableTo": "layout", + "columnsFrom": [ + "layout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "item_layout_item_id_section_id_layout_id_pk": { + "columns": [ + "item_id", + "section_id", + "layout_id" + ], + "name": "item_layout_item_id_section_id_layout_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "item": { + "name": "item", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "board_id": { + "name": "board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{\"json\": {}}'" + }, + "advanced_options": { + "name": "advanced_options", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{\"json\": {}}'" + } + }, + "indexes": {}, + "foreignKeys": { + "item_board_id_board_id_fk": { + "name": "item_board_id_board_id_fk", + "tableFrom": "item", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "layout": { + "name": "layout", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "board_id": { + "name": "board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "column_count": { + "name": "column_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "breakpoint": { + "name": "breakpoint", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "layout_board_id_board_id_fk": { + "name": "layout_board_id_board_id_fk", + "tableFrom": "layout", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "media": { + "name": "media", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "media_creator_id_user_id_fk": { + "name": "media_creator_id_user_id_fk", + "tableFrom": "media", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "onboarding": { + "name": "onboarding", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "step": { + "name": "step", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "previous_step": { + "name": "previous_step", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "search_engine": { + "name": "search_engine", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "short": { + "name": "short", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url_template": { + "name": "url_template", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'generic'" + }, + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "search_engine_short_unique": { + "name": "search_engine_short_unique", + "columns": [ + "short" + ], + "isUnique": true + } + }, + "foreignKeys": { + "search_engine_integration_id_integration_id_fk": { + "name": "search_engine_integration_id_integration_id_fk", + "tableFrom": "search_engine", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "section_collapse_state": { + "name": "section_collapse_state", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "collapsed": { + "name": "collapsed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "section_collapse_state_user_id_user_id_fk": { + "name": "section_collapse_state_user_id_user_id_fk", + "tableFrom": "section_collapse_state", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_collapse_state_section_id_section_id_fk": { + "name": "section_collapse_state_section_id_section_id_fk", + "tableFrom": "section_collapse_state", + "tableTo": "section", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "section_collapse_state_user_id_section_id_pk": { + "columns": [ + "user_id", + "section_id" + ], + "name": "section_collapse_state_user_id_section_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "section_layout": { + "name": "section_layout", + "columns": { + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout_id": { + "name": "layout_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_section_id": { + "name": "parent_section_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "x_offset": { + "name": "x_offset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "y_offset": { + "name": "y_offset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "section_layout_section_id_section_id_fk": { + "name": "section_layout_section_id_section_id_fk", + "tableFrom": "section_layout", + "tableTo": "section", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_layout_layout_id_layout_id_fk": { + "name": "section_layout_layout_id_layout_id_fk", + "tableFrom": "section_layout", + "tableTo": "layout", + "columnsFrom": [ + "layout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_layout_parent_section_id_section_id_fk": { + "name": "section_layout_parent_section_id_section_id_fk", + "tableFrom": "section_layout", + "tableTo": "section", + "columnsFrom": [ + "parent_section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "section_layout_section_id_layout_id_pk": { + "columns": [ + "section_id", + "layout_id" + ], + "name": "section_layout_section_id_layout_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "section": { + "name": "section", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "board_id": { + "name": "board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "x_offset": { + "name": "x_offset", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "y_offset": { + "name": "y_offset", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'{\"json\": {}}'" + } + }, + "indexes": {}, + "foreignKeys": { + "section_board_id_board_id_fk": { + "name": "section_board_id_board_id_fk", + "tableFrom": "section", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serverSetting": { + "name": "serverSetting", + "columns": { + "setting_key": { + "name": "setting_key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{\"json\": {}}'" + } + }, + "indexes": { + "serverSetting_settingKey_unique": { + "name": "serverSetting_settingKey_unique", + "columns": [ + "setting_key" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_id_idx": { + "name": "user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "trusted_certificate_hostname": { + "name": "trusted_certificate_hostname", + "columns": { + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "thumbprint": { + "name": "thumbprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "certificate": { + "name": "certificate", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "trusted_certificate_hostname_hostname_thumbprint_pk": { + "columns": [ + "hostname", + "thumbprint" + ], + "name": "trusted_certificate_hostname_hostname_thumbprint_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credentials'" + }, + "home_board_id": { + "name": "home_board_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mobile_home_board_id": { + "name": "mobile_home_board_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_search_engine_id": { + "name": "default_search_engine_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "open_search_in_new_tab": { + "name": "open_search_in_new_tab", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "ddg_bangs": { + "name": "ddg_bangs", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "color_scheme": { + "name": "color_scheme", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'dark'" + }, + "first_day_of_week": { + "name": "first_day_of_week", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "ping_icons_enabled": { + "name": "ping_icons_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "completed_manage_tour": { + "name": "completed_manage_tour", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "completed_board_tour": { + "name": "completed_board_tour", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_home_board_id_board_id_fk": { + "name": "user_home_board_id_board_id_fk", + "tableFrom": "user", + "tableTo": "board", + "columnsFrom": [ + "home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_mobile_home_board_id_board_id_fk": { + "name": "user_mobile_home_board_id_board_id_fk", + "tableFrom": "user", + "tableTo": "board", + "columnsFrom": [ + "mobile_home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_default_search_engine_id_search_engine_id_fk": { + "name": "user_default_search_engine_id_search_engine_id_fk", + "tableFrom": "user", + "tableTo": "search_engine", + "columnsFrom": [ + "default_search_engine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "columns": [ + "identifier", + "token" + ], + "name": "verificationToken_identifier_token_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/packages/db/migrations/sqlite/meta/0040_snapshot.json b/packages/db/migrations/sqlite/meta/0040_snapshot.json index e1fb68339..9f0bc677e 100644 --- a/packages/db/migrations/sqlite/meta/0040_snapshot.json +++ b/packages/db/migrations/sqlite/meta/0040_snapshot.json @@ -1,8 +1,8 @@ { "version": "6", "dialect": "sqlite", - "id": "b02e04f9-1462-4832-9a55-b26aded09a36", - "prevId": "78c75619-4e39-4774-9f91-573f629144d0", + "id": "d76abdce-ba49-450b-81f0-621bcf581b90", + "prevId": "661fcf56-12b6-48a9-8d70-24032f0ec338", "tables": { "account": { "name": "account", @@ -589,13 +589,6 @@ "notNull": false, "autoincrement": false }, - "url": { - "name": "url", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, "auth_type": { "name": "auth_type", "type": "text", @@ -642,14 +635,6 @@ "autoincrement": false, "default": "'{\"json\": {}}'" }, - "enabled": { - "name": "enabled", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - }, "created_at": { "name": "created_at", "type": "integer", @@ -672,6 +657,21 @@ "primaryKey": false, "notNull": false, "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true } }, "indexes": {}, @@ -2440,6 +2440,66 @@ }, "uniqueConstraints": {}, "checkConstraints": {} + }, + "widget_secret": { + "name": "widget_secret", + "columns": { + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "widget_secret_item_id_item_id_fk": { + "name": "widget_secret_item_id_item_id_fk", + "tableFrom": "widget_secret", + "tableTo": "item", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "widget_secret_item_id_kind_pk": { + "columns": [ + "item_id", + "kind" + ], + "name": "widget_secret_item_id_kind_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} } }, "views": {}, @@ -2447,11 +2507,9 @@ "_meta": { "schemas": {}, "tables": {}, - "columns": { - "\"custom_widget_definition\".\"base_url\"": "\"custom_widget_definition\".\"url\"" - } + "columns": {} }, "internal": { "indexes": {} } -} \ No newline at end of file +} diff --git a/packages/db/migrations/sqlite/meta/_journal.json b/packages/db/migrations/sqlite/meta/_journal.json index c469ca9e2..a99a995ce 100644 --- a/packages/db/migrations/sqlite/meta/_journal.json +++ b/packages/db/migrations/sqlite/meta/_journal.json @@ -285,15 +285,8 @@ { "idx": 40, "version": "6", - "when": 1782836728121, - "tag": "0040_charming_tag", - "breakpoints": true - }, - { - "idx": 41, - "version": "6", - "when": 1782837354691, - "tag": "0041_adorable_marten_broadcloak", + "when": 1783176100870, + "tag": "0040_add_widget_secret", "breakpoints": true } ] diff --git a/packages/db/schema/index.ts b/packages/db/schema/index.ts index 59566631f..8c3b7f4fc 100644 --- a/packages/db/schema/index.ts +++ b/packages/db/schema/index.ts @@ -51,6 +51,7 @@ export const { cronJobConfigurations, customWidgetDefinitions, customWidgetSecrets, + widgetSecrets, } = schema; export type User = InferSelectModel; diff --git a/packages/db/schema/mysql.ts b/packages/db/schema/mysql.ts index 0549017de..97cf25d4e 100644 --- a/packages/db/schema/mysql.ts +++ b/packages/db/schema/mysql.ts @@ -542,6 +542,25 @@ export const customWidgetSecrets = mysqlTable( }), ); +export const widgetSecrets = mysqlTable( + "widget_secret", + { + itemId: varchar({ length: 64 }) + .notNull() + .references(() => items.id, { onDelete: "cascade" }), + kind: varchar({ length: 64 }).notNull(), + value: text().$type<`${string}.${string}`>().notNull(), + updatedAt: timestamp() + .$onUpdateFn(() => new Date()) + .notNull(), + }, + (table) => ({ + compoundKey: primaryKey({ + columns: [table.itemId, table.kind], + }), + }), +); + export const cronJobConfigurations = mysqlTable("cron_job_configuration", { name: varchar({ length: 256 }).notNull().primaryKey(), cronExpression: varchar({ length: 32 }).notNull(), @@ -751,12 +770,20 @@ export const sectionCollapseStateRelations = relations(sectionCollapseStates, ({ export const itemRelations = relations(items, ({ one, many }) => ({ integrations: many(integrationItems), layouts: many(itemLayouts), + secrets: many(widgetSecrets), board: one(boards, { fields: [items.boardId], references: [boards.id], }), })); +export const widgetSecretRelations = relations(widgetSecrets, ({ one }) => ({ + item: one(items, { + fields: [widgetSecrets.itemId], + references: [items.id], + }), +})); + export const integrationItemRelations = relations(integrationItems, ({ one }) => ({ integration: one(integrations, { fields: [integrationItems.integrationId], diff --git a/packages/db/schema/postgresql.ts b/packages/db/schema/postgresql.ts index 6a2535956..caf0659ed 100644 --- a/packages/db/schema/postgresql.ts +++ b/packages/db/schema/postgresql.ts @@ -541,6 +541,25 @@ export const customWidgetSecrets = pgTable( }), ); +export const widgetSecrets = pgTable( + "widget_secret", + { + itemId: varchar({ length: 64 }) + .notNull() + .references(() => items.id, { onDelete: "cascade" }), + kind: varchar({ length: 64 }).notNull(), + value: text().$type<`${string}.${string}`>().notNull(), + updatedAt: timestamp() + .$onUpdateFn(() => new Date()) + .notNull(), + }, + (table) => ({ + compoundKey: primaryKey({ + columns: [table.itemId, table.kind], + }), + }), +); + export const cronJobConfigurations = pgTable("cron_job_configuration", { name: varchar({ length: 256 }).notNull().primaryKey(), cronExpression: varchar({ length: 32 }).notNull(), @@ -750,12 +769,20 @@ export const sectionCollapseStateRelations = relations(sectionCollapseStates, ({ export const itemRelations = relations(items, ({ one, many }) => ({ integrations: many(integrationItems), layouts: many(itemLayouts), + secrets: many(widgetSecrets), board: one(boards, { fields: [items.boardId], references: [boards.id], }), })); +export const widgetSecretRelations = relations(widgetSecrets, ({ one }) => ({ + item: one(items, { + fields: [widgetSecrets.itemId], + references: [items.id], + }), +})); + export const integrationItemRelations = relations(integrationItems, ({ one }) => ({ integration: one(integrations, { fields: [integrationItems.integrationId], diff --git a/packages/db/schema/sqlite.ts b/packages/db/schema/sqlite.ts index 95080f67a..5aa2fa61a 100644 --- a/packages/db/schema/sqlite.ts +++ b/packages/db/schema/sqlite.ts @@ -531,6 +531,25 @@ export const customWidgetSecrets = sqliteTable( }), ); +export const widgetSecrets = sqliteTable( + "widget_secret", + { + itemId: text() + .notNull() + .references(() => items.id, { onDelete: "cascade" }), + kind: text().notNull(), + value: text().$type<`${string}.${string}`>().notNull(), + updatedAt: int({ mode: "timestamp" }) + .$onUpdateFn(() => new Date()) + .notNull(), + }, + (table) => ({ + compoundKey: primaryKey({ + columns: [table.itemId, table.kind], + }), + }), +); + export const cronJobConfigurations = sqliteTable("cron_job_configuration", { name: text().notNull().primaryKey(), cronExpression: text().notNull(), @@ -740,12 +759,20 @@ export const sectionCollapseStateRelations = relations(sectionCollapseStates, ({ export const itemRelations = relations(items, ({ one, many }) => ({ integrations: many(integrationItems), layouts: many(itemLayouts), + secrets: many(widgetSecrets), board: one(boards, { fields: [items.boardId], references: [boards.id], }), })); +export const widgetSecretRelations = relations(widgetSecrets, ({ one }) => ({ + item: one(items, { + fields: [widgetSecrets.itemId], + references: [items.id], + }), +})); + export const integrationItemRelations = relations(integrationItems, ({ one }) => ({ integration: one(integrations, { fields: [integrationItems.integrationId], diff --git a/packages/definitions/src/docker-integration-match.ts b/packages/definitions/src/docker-integration-match.ts index 9dcba12d4..36a0d0f58 100644 --- a/packages/definitions/src/docker-integration-match.ts +++ b/packages/definitions/src/docker-integration-match.ts @@ -31,11 +31,11 @@ const integrationAliases: Partial> = audiobookshelf: ["audiobookshelf"], navidrome: ["navidrome"], paperlessNgx: ["paperless-ngx", "paperless"], - searchCh: ["search-ch"], - gitHubContainerRegistry: ["ghcr"], - linuxServerIO: ["linuxserver"], + patchmon: ["patchmon", "patch-mon"], coolify: ["coolify"], truenas: ["truenas"], + bazarr: ["bazarr"], + synology: ["synology", "diskstation"], }; export const matchIntegrationKind = (search: string): IntegrationKind | null => { @@ -63,19 +63,7 @@ export const matchIntegrationKind = (search: string): IntegrationKind | null => return null; }; -const notDockerDiscoverable = new Set([ - "dockerHub", - "github", - "gitlab", - "npm", - "codeberg", - "linuxServerIO", - "gitHubContainerRegistry", - "quay", - "ical", - "searchCh", - "mock", -]); +const notDockerDiscoverable = new Set(["ical", "mock"]); interface ContainerMatchInput { image: string; diff --git a/packages/definitions/src/docs/homarr-docs-sitemap.ts b/packages/definitions/src/docs/homarr-docs-sitemap.ts index d78e36782..f01b934cd 100644 --- a/packages/definitions/src/docs/homarr-docs-sitemap.ts +++ b/packages/definitions/src/docs/homarr-docs-sitemap.ts @@ -60,8 +60,8 @@ export type HomarrDocumentationPath = | "/docs/tags/connections" | "/docs/tags/custom-widgets" | "/docs/tags/customization" - | "/docs/tags/data-sources" | "/docs/tags/data" + | "/docs/tags/data-sources" | "/docs/tags/database" | "/docs/tags/developer" | "/docs/tags/development" @@ -99,8 +99,8 @@ export type HomarrDocumentationPath = | "/docs/tags/redis" | "/docs/tags/responsive" | "/docs/tags/roles" - | "/docs/tags/search-engines" | "/docs/tags/search" + | "/docs/tags/search-engines" | "/docs/tags/secrets" | "/docs/tags/security" | "/docs/tags/self-signed" @@ -132,15 +132,6 @@ export type HomarrDocumentationPath = | "/docs/advanced/running-as-different-user" | "/docs/advanced/single-sign-on" | "/docs/advanced/styling" - | "/docs/category/advanced" - | "/docs/category/community" - | "/docs/category/developer-guides" - | "/docs/category/getting-started" - | "/docs/category/installation-1" - | "/docs/category/installation" - | "/docs/category/integrations" - | "/docs/category/management" - | "/docs/category/widgets" | "/docs/community/donate" | "/docs/community/faq" | "/docs/community/get-in-touch" @@ -168,17 +159,13 @@ export type HomarrDocumentationPath = | "/docs/integrations/archiveteam-warrior" | "/docs/integrations/aria2" | "/docs/integrations/audiobookshelf" + | "/docs/integrations/bazarr" | "/docs/integrations/beszel" - | "/docs/integrations/codeberg" | "/docs/integrations/coolify" | "/docs/integrations/dash-dot" | "/docs/integrations/deluge" - | "/docs/integrations/docker-hub" | "/docs/integrations/docker" | "/docs/integrations/emby" - | "/docs/integrations/github-containerregistry" - | "/docs/integrations/github" - | "/docs/integrations/gitlab" | "/docs/integrations/glances" | "/docs/integrations/gluetun" | "/docs/integrations/gotify" @@ -189,34 +176,33 @@ export type HomarrDocumentationPath = | "/docs/integrations/jellyseerr" | "/docs/integrations/kubernetes" | "/docs/integrations/lidarr" - | "/docs/integrations/linux-server-io" | "/docs/integrations/navidrome" | "/docs/integrations/nextcloud" - | "/docs/integrations/npm" | "/docs/integrations/ntfy" | "/docs/integrations/nzbget" | "/docs/integrations/open-media-vault" | "/docs/integrations/opnsense" | "/docs/integrations/overseerr" | "/docs/integrations/paperless-ngx" + | "/docs/integrations/patchmon" | "/docs/integrations/peanut" | "/docs/integrations/pi-hole" | "/docs/integrations/plex" | "/docs/integrations/prowlarr" | "/docs/integrations/proxmox" | "/docs/integrations/q-bittorent" - | "/docs/integrations/quay" | "/docs/integrations/radarr" | "/docs/integrations/readarr" | "/docs/integrations/sabnzbd" - | "/docs/integrations/search-ch" | "/docs/integrations/seerr" | "/docs/integrations/slskd" | "/docs/integrations/sonarr" | "/docs/integrations/speedtest-tracker" + | "/docs/integrations/synology" | "/docs/integrations/tdarr" | "/docs/integrations/technitium-dns" | "/docs/integrations/tracearr" + | "/docs/integrations/traefik" | "/docs/integrations/transmission" | "/docs/integrations/truenas" | "/docs/integrations/umami" @@ -239,6 +225,7 @@ export type HomarrDocumentationPath = | "/docs/widgets/anchor-note" | "/docs/widgets/app" | "/docs/widgets/audio-stats" + | "/docs/widgets/bazarr" | "/docs/widgets/beszel-alerts" | "/docs/widgets/beszel-system-grid" | "/docs/widgets/beszel-system-stats" @@ -258,6 +245,7 @@ export type HomarrDocumentationPath = | "/docs/widgets/immich-album-carousel" | "/docs/widgets/immich-server-stats" | "/docs/widgets/indexer-manager" + | "/docs/widgets/media-missing" | "/docs/widgets/media-releases" | "/docs/widgets/media-request-list" | "/docs/widgets/media-request-stats" @@ -269,15 +257,18 @@ export type HomarrDocumentationPath = | "/docs/widgets/notebook" | "/docs/widgets/notifications" | "/docs/widgets/paperless-ngx" + | "/docs/widgets/patchmon" | "/docs/widgets/releases" | "/docs/widgets/rss-feed" | "/docs/widgets/smart-home-entity-state" | "/docs/widgets/smart-home-execute-automation" | "/docs/widgets/speedtest-tracker" | "/docs/widgets/stock-price" + | "/docs/widgets/system-disks" | "/docs/widgets/system-resources" | "/docs/widgets/timetable" | "/docs/widgets/tracearr" + | "/docs/widgets/traefik" | "/docs/widgets/umami" | "/docs/widgets/ups" | "/docs/widgets/uptime-kuma" @@ -298,6 +289,7 @@ export type HomarrDocumentationPath = | "/docs/integrations/lidarr" | "/docs/integrations/readarr" | "/docs/integrations/prowlarr" + | "/docs/integrations/bazarr" | "/docs/integrations/jellyfin" | "/docs/integrations/emby" | "/docs/integrations/plex" @@ -316,24 +308,17 @@ export type HomarrDocumentationPath = | "/docs/integrations/nextcloud" | "/docs/integrations/unifi-controller" | "/docs/integrations/opnsense" - | "/docs/integrations/github" - | "/docs/integrations/docker-hub" - | "/docs/integrations/gitlab" - | "/docs/integrations/npm" - | "/docs/integrations/codeberg" - | "/docs/integrations/linux-server-io" - | "/docs/integrations/github-containerregistry" - | "/docs/integrations/quay" | "/docs/integrations/ntfy" | "/docs/integrations/gotify" | "/docs/integrations/ical" | "/docs/integrations/anchor" | "/docs/integrations/truenas" + | "/docs/integrations/synology" | "/docs/integrations/unraid" | "/docs/integrations/coolify" - | "/docs/integrations/search-ch" | "/docs/integrations/immich" | "/docs/integrations/paperless-ngx" + | "/docs/integrations/patchmon" | "/docs/integrations/tracearr" | "/docs/integrations/speedtest-tracker" | "/docs/integrations/uptime-kuma" @@ -344,6 +329,7 @@ export type HomarrDocumentationPath = | "/docs/integrations/peanut" | "/docs/integrations/beszel" | "/docs/integrations/open-webui" + | "/docs/integrations/traefik" | "/docs/widgets/clock" | "/docs/widgets/weather" | "/docs/widgets/app" @@ -362,6 +348,7 @@ export type HomarrDocumentationPath = | "/docs/widgets/media-request-list" | "/docs/widgets/media-request-stats" | "/docs/widgets/media-transcoding" + | "/docs/widgets/media-missing" | "/docs/widgets/minecraft-server-status" | "/docs/widgets/network-controller-summary" | "/docs/widgets/network-controller-status" @@ -376,10 +363,13 @@ export type HomarrDocumentationPath = | "/docs/widgets/notifications" | "/docs/widgets/system-resources" | "/docs/widgets/coolify" + | "/docs/widgets/system-disks" | "/docs/widgets/timetable" | "/docs/widgets/immich-server-stats" | "/docs/widgets/immich-album-carousel" | "/docs/widgets/paperless-ngx" + | "/docs/widgets/patchmon" + | "/docs/widgets/bazarr" | "/docs/widgets/tracearr" | "/docs/widgets/speedtest-tracker" | "/docs/widgets/uptime-kuma" @@ -391,5 +381,6 @@ export type HomarrDocumentationPath = | "/docs/widgets/beszel-system-grid" | "/docs/widgets/beszel-alerts" | "/docs/widgets/beszel-system-stats" + | "/docs/widgets/traefik" | "/docs/widgets/custom-api" | "/docs/widgets/open-webui"; diff --git a/packages/definitions/src/docs/integration-doc-slugs.ts b/packages/definitions/src/docs/integration-doc-slugs.ts index d081effe1..6cd135c50 100644 --- a/packages/definitions/src/docs/integration-doc-slugs.ts +++ b/packages/definitions/src/docs/integration-doc-slugs.ts @@ -13,6 +13,7 @@ export const integrationDocSlugs: Record = { lidarr: "lidarr", readarr: "readarr", prowlarr: "prowlarr", + bazarr: "bazarr", jellyfin: "jellyfin", emby: "emby", plex: "plex", @@ -31,24 +32,17 @@ export const integrationDocSlugs: Record = { nextcloud: "nextcloud", unifiController: "unifi-controller", opnsense: "opnsense", - github: "github", - dockerHub: "docker-hub", - gitlab: "gitlab", - npm: "npm", - codeberg: "codeberg", - linuxServerIO: "linux-server-io", - gitHubContainerRegistry: "github-containerregistry", - quay: "quay", ntfy: "ntfy", gotify: "gotify", ical: "ical", anchor: "anchor", truenas: "truenas", + synology: "synology", unraid: "unraid", coolify: "coolify", - searchCh: "search-ch", immich: "immich", paperlessNgx: "paperless-ngx", + patchmon: "patchmon", tracearr: "tracearr", speedtestTracker: "speedtest-tracker", uptimeKuma: "uptime-kuma", @@ -58,6 +52,7 @@ export const integrationDocSlugs: Record = { gluetun: "gluetun", peaNut: "peanut", beszel: "beszel", + traefik: "traefik", archiveTeamWarrior: null, openWebUi: "open-webui", mock: null, diff --git a/packages/definitions/src/docs/widget-doc-slugs.ts b/packages/definitions/src/docs/widget-doc-slugs.ts index 347e1b90a..3d9c0754a 100644 --- a/packages/definitions/src/docs/widget-doc-slugs.ts +++ b/packages/definitions/src/docs/widget-doc-slugs.ts @@ -19,6 +19,7 @@ export const widgetDocSlugs: Record = { "mediaRequests-requestList": "media-request-list", "mediaRequests-requestStats": "media-request-stats", mediaTranscoding: "media-transcoding", + mediaMissing: "media-missing", minecraftServerStatus: "minecraft-server-status", networkControllerSummary: "network-controller-summary", networkControllerStatus: "network-controller-status", @@ -33,11 +34,13 @@ export const widgetDocSlugs: Record = { notifications: "notifications", systemResources: "system-resources", coolify: "coolify", - systemDisks: null, + systemDisks: "system-disks", timetable: "timetable", "immich-serverStats": "immich-server-stats", "immich-albumCarousel": "immich-album-carousel", paperlessNgx: "paperless-ngx", + patchmon: "patchmon", + bazarr: "bazarr", tracearr: "tracearr", speedtestTracker: "speedtest-tracker", uptimeKuma: "uptime-kuma", @@ -50,6 +53,7 @@ export const widgetDocSlugs: Record = { beszelSystemGrid: "beszel-system-grid", beszelAlerts: "beszel-alerts", beszelSystemStats: "beszel-system-stats", + traefik: "traefik", customApi: "custom-api", openWebUi: "open-webui", }; diff --git a/packages/definitions/src/index.ts b/packages/definitions/src/index.ts index eb81e4d18..94b392ee7 100644 --- a/packages/definitions/src/index.ts +++ b/packages/definitions/src/index.ts @@ -17,3 +17,4 @@ export * from "./emptysuperjson"; export * from "./hotkeys"; export * from "./docker-integration-match"; export * from "./integration-url-template"; +export * from "./release-provider"; diff --git a/packages/definitions/src/integration.ts b/packages/definitions/src/integration.ts index 55646c919..d14bea960 100644 --- a/packages/definitions/src/integration.ts +++ b/packages/definitions/src/integration.ts @@ -13,6 +13,8 @@ export const integrationSecretKindObject = { topic: { isPublic: true, multiline: false }, opnsenseApiKey: { isPublic: false, multiline: false }, opnsenseApiSecret: { isPublic: false, multiline: false }, + patchmonApiKey: { isPublic: true, multiline: false }, + patchmonApiSecret: { isPublic: false, multiline: false }, url: { isPublic: false, multiline: false }, privateKey: { isPublic: false, multiline: true }, githubAppId: { isPublic: true, multiline: false }, @@ -96,7 +98,7 @@ export const integrationDefs = { name: "Sonarr", secretKinds: [["apiKey"]], iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/sonarr.svg", - category: ["calendar"], + category: ["calendar", "mediaOrganizer"], documentationUrl: createDocumentationLink("/docs/integrations/sonarr"), defaultPort: 8989, apiKeySettingsPath: "/settings/general", @@ -105,7 +107,7 @@ export const integrationDefs = { name: "Radarr", secretKinds: [["apiKey"]], iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/radarr.svg", - category: ["calendar"], + category: ["calendar", "mediaOrganizer"], documentationUrl: createDocumentationLink("/docs/integrations/radarr"), defaultPort: 7878, apiKeySettingsPath: "/settings/general", @@ -137,6 +139,15 @@ export const integrationDefs = { defaultPort: 9696, apiKeySettingsPath: "/settings/general", }, + bazarr: { + name: "Bazarr", + secretKinds: [["apiKey"]], + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/bazarr.svg", + category: ["subtitleManager"], + documentationUrl: createDocumentationLink("/docs/integrations/bazarr"), + defaultPort: 6767, + apiKeySettingsPath: "/settings/general", + }, jellyfin: { name: "Jellyfin", secretKinds: [["username", "password"], ["apiKey"]], @@ -283,70 +294,6 @@ export const integrationDefs = { documentationUrl: createDocumentationLink("/docs/integrations/opnsense"), defaultPort: 443, }, - github: { - name: "Github", - secretKinds: [[], ["personalAccessToken"], ["githubAppId", "githubInstallationId", "privateKey"]], - iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/github.svg", - category: ["releasesProvider"], - defaultUrl: "https://api.github.com", - documentationUrl: createDocumentationLink("/docs/integrations/github"), - }, - dockerHub: { - name: "Docker Hub", - secretKinds: [[], ["username", "personalAccessToken"]], - iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/docker.svg", - category: ["releasesProvider"], - defaultUrl: "https://hub.docker.com", - documentationUrl: createDocumentationLink("/docs/integrations/docker-hub"), - }, - gitlab: { - name: "Gitlab", - secretKinds: [[], ["personalAccessToken"]], - iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/gitlab.svg", - category: ["releasesProvider"], - defaultUrl: "https://gitlab.com", - documentationUrl: createDocumentationLink("/docs/integrations/gitlab"), - }, - npm: { - name: "NPM", - secretKinds: [[]], - iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/npm.svg", - category: ["releasesProvider"], - defaultUrl: "https://registry.npmjs.org", - documentationUrl: createDocumentationLink("/docs/integrations/npm"), - }, - codeberg: { - name: "Codeberg", - secretKinds: [[], ["personalAccessToken"]], - iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/codeberg.svg", - category: ["releasesProvider"], - defaultUrl: "https://codeberg.org", - documentationUrl: createDocumentationLink("/docs/integrations/codeberg"), - }, - linuxServerIO: { - name: "LinuxServer.io", - secretKinds: [[]], - iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/linuxserver-io.svg", - category: ["releasesProvider"], - defaultUrl: "https://api.linuxserver.io", - documentationUrl: createDocumentationLink("/docs/integrations/linux-server-io"), - }, - gitHubContainerRegistry: { - name: "GitHub Container Registry", - secretKinds: [[], ["personalAccessToken"], ["githubAppId", "githubInstallationId", "privateKey"]], - iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/github.svg", - category: ["releasesProvider"], - defaultUrl: "https://api.github.com", - documentationUrl: createDocumentationLink("/docs/integrations/github"), - }, - quay: { - name: "Quay", - secretKinds: [[], ["personalAccessToken"]], - iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/quay.png", - category: ["releasesProvider"], - defaultUrl: "https://quay.io", - documentationUrl: createDocumentationLink("/docs/integrations/quay"), - }, ntfy: { name: "ntfy", secretKinds: [["topic"], ["topic", "apiKey"]], @@ -385,6 +332,14 @@ export const integrationDefs = { documentationUrl: createDocumentationLink("/docs/integrations/truenas"), defaultPort: 80, }, + synology: { + name: "Synology DiskStation", + secretKinds: [["username", "password"]], + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/synology.svg", + category: ["healthMonitoring"], + documentationUrl: createDocumentationLink("/docs/integrations/synology"), + defaultPort: 5000, + }, unraid: { name: "Unraid", secretKinds: [["apiKey"]], @@ -401,13 +356,6 @@ export const integrationDefs = { documentationUrl: createDocumentationLink("/docs/integrations/coolify"), defaultPort: 8000, }, - searchCh: { - name: "Search.ch", - secretKinds: [[]], - iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/search-ch.png", - category: ["timetable"], - documentationUrl: createDocumentationLink("/docs/integrations/search-ch"), - }, immich: { name: "Immich", secretKinds: [["apiKey"]], @@ -424,6 +372,15 @@ export const integrationDefs = { documentationUrl: null, defaultPort: 8000, }, + patchmon: { + name: "PatchMon", + secretKinds: [["patchmonApiKey", "patchmonApiSecret"]], + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/patchmon.svg", + category: ["healthMonitoring"], + documentationUrl: createDocumentationLink("/docs/integrations/patchmon"), + defaultPort: 3413, + apiKeySettingsPath: "/settings/api-keys", + }, tracearr: { name: "Tracearr", secretKinds: [["apiKey"]], @@ -496,13 +453,21 @@ export const integrationDefs = { category: ["vpn"], documentationUrl: createDocumentationLink("/docs/integrations/gluetun"), }, + traefik: { + name: "Traefik", + secretKinds: [[], ["username", "password"], ["apiKey"]], + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/traefik.svg", + category: ["reverseProxy"], + documentationUrl: createDocumentationLink("/docs/integrations/traefik"), + defaultPort: 8080, + }, archiveTeamWarrior: { name: "ArchiveTeam Warrior", secretKinds: [[], ["username", "password"]], iconUrl: "https://cdn.jsdelivr.net/gh/selfhst/icons/png/archiveteam-warrior.png", category: ["archiving"], - defaultUrl: "http://localhost:8001", documentationUrl: null, + defaultUrl: "http://localhost:8001", }, openWebUi: { name: "Open WebUI", @@ -533,6 +498,7 @@ export const integrationDefs = { "smartHomeServer", "ups", "uptimeMonitoring", + "mediaOrganizer", ], documentationUrl: null, }, @@ -598,6 +564,7 @@ export const integrationCategories = [ "dnsHole", "mediaService", "calendar", + "mediaOrganizer", "mediaSearch", "mediaRelease", "mediaRequest", @@ -611,7 +578,6 @@ export const integrationCategories = [ "search", "mediaTranscoding", "networkController", - "releasesProvider", "notifications", "firewall", "timetable", @@ -627,6 +593,8 @@ export const integrationCategories = [ "mediaLibrary", "uptimeMonitoring", "ai", + "subtitleManager", + "reverseProxy", ] as const; export type IntegrationCategory = (typeof integrationCategories)[number]; diff --git a/packages/definitions/src/release-provider.ts b/packages/definitions/src/release-provider.ts new file mode 100644 index 000000000..3fa53cc8f --- /dev/null +++ b/packages/definitions/src/release-provider.ts @@ -0,0 +1,78 @@ +import { objectKeys } from "@homarr/common"; + +export const releaseProviderDefs = { + github: { + name: "Github", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/github.svg", + defaultUrl: "https://api.github.com", + }, + dockerHub: { + name: "Docker Hub", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/docker.svg", + defaultUrl: "https://hub.docker.com", + }, + gitlab: { + name: "Gitlab", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/gitlab.svg", + defaultUrl: "https://gitlab.com", + }, + npm: { + name: "NPM", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/npm.svg", + defaultUrl: "https://registry.npmjs.org", + }, + codeberg: { + name: "Codeberg", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/codeberg.svg", + defaultUrl: "https://codeberg.org", + }, + linuxServerIO: { + name: "LinuxServer.io", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/linuxserver-io.svg", + defaultUrl: "https://api.linuxserver.io", + }, + gitHubContainerRegistry: { + name: "GitHub Container Registry", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/github.svg", + defaultUrl: "https://ghcr.io", + }, + quay: { + name: "Quay", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/quay.png", + defaultUrl: "https://quay.io", + }, +} as const; + +export const releaseProviderKinds = objectKeys(releaseProviderDefs); +export type ReleaseProviderKind = keyof typeof releaseProviderDefs; + +export const getReleaseProviderName = (provider: ReleaseProviderKind) => releaseProviderDefs[provider].name; +export const getReleaseProviderIconUrl = (provider: ReleaseProviderKind) => releaseProviderDefs[provider].iconUrl; +export const getReleaseProviderDefaultUrl = (provider: ReleaseProviderKind) => releaseProviderDefs[provider].defaultUrl; + +const releaseProviderRegistryPrefixes: Partial> = { + dockerHub: "docker.io/", + gitHubContainerRegistry: "ghcr.io/", + linuxServerIO: "lscr.io/", + quay: "quay.io/", +}; + +const removeContainerImageVersion = (identifier: string) => { + const digestIndex = identifier.indexOf("@"); + const withoutDigest = digestIndex === -1 ? identifier : identifier.slice(0, digestIndex); + const tagIndex = withoutDigest.indexOf(":", withoutDigest.lastIndexOf("/") + 1); + + return tagIndex === -1 ? withoutDigest : withoutDigest.slice(0, tagIndex); +}; + +export const normalizeReleaseProviderIdentifier = (provider: ReleaseProviderKind, identifier: string) => { + const trimmedIdentifier = identifier.trim(); + const registryPrefix = releaseProviderRegistryPrefixes[provider]; + const withoutRegistry = + registryPrefix && trimmedIdentifier.startsWith(registryPrefix) + ? trimmedIdentifier.slice(registryPrefix.length) + : trimmedIdentifier; + + // ponytail: only strip container image versions/digests for providers that use them + return registryPrefix ? removeContainerImageVersion(withoutRegistry) : withoutRegistry; +}; diff --git a/packages/definitions/src/test/release-provider.spec.ts b/packages/definitions/src/test/release-provider.spec.ts new file mode 100644 index 000000000..7e7e0b90e --- /dev/null +++ b/packages/definitions/src/test/release-provider.spec.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "vitest"; + +import { normalizeReleaseProviderIdentifier } from "../release-provider"; + +describe("normalizeReleaseProviderIdentifier", () => { + it("normalizes full ghcr image references", () => { + expect(normalizeReleaseProviderIdentifier("gitHubContainerRegistry", "ghcr.io/muchobien/pocketbase:latest")).toBe( + "muchobien/pocketbase", + ); + expect(normalizeReleaseProviderIdentifier("gitHubContainerRegistry", "ghcr.io/homarr-labs/homarr:latest")).toBe( + "homarr-labs/homarr", + ); + }); + + it("keeps existing ghcr owner/name identifiers unchanged", () => { + expect(normalizeReleaseProviderIdentifier("gitHubContainerRegistry", "homarr-labs/homarr")).toBe( + "homarr-labs/homarr", + ); + }); + + it("normalizes container image identifiers for other registries", () => { + expect(normalizeReleaseProviderIdentifier("dockerHub", "postgres:17")).toBe("postgres"); + expect(normalizeReleaseProviderIdentifier("dockerHub", "docker.io/library/postgres:17")).toBe("library/postgres"); + expect(normalizeReleaseProviderIdentifier("dockerHub", "library/postgres@sha256:abc123")).toBe("library/postgres"); + expect(normalizeReleaseProviderIdentifier("linuxServerIO", "lscr.io/linuxserver/radarr:latest")).toBe( + "linuxserver/radarr", + ); + expect(normalizeReleaseProviderIdentifier("quay", "quay.io/prometheus/prometheus:v3.0.0")).toBe( + "prometheus/prometheus", + ); + }); +}); diff --git a/packages/definitions/src/widget-integration-map.ts b/packages/definitions/src/widget-integration-map.ts index 9d85c250b..fcec5a905 100644 --- a/packages/definitions/src/widget-integration-map.ts +++ b/packages/definitions/src/widget-integration-map.ts @@ -20,27 +20,31 @@ export const widgetIntegrationSupport: Partial kind !== "patchmon"), firewall: getIntegrationKindsByCategory("firewall"), notifications: getIntegrationKindsByCategory("notifications"), mediaReleases: ["emby", "jellyfin", "plex"], - systemResources: ["dashDot", "openmediavault", "truenas", "unraid", "glances"], - systemDisks: ["dashDot", "openmediavault", "truenas", "unraid"], + systemResources: ["dashDot", "openmediavault", "truenas", "unraid", "glances", "synology"], + systemDisks: ["dashDot", "openmediavault", "truenas", "unraid", "synology"], coolify: ["coolify"], - timetable: ["searchCh"], "immich-serverStats": ["immich"], "immich-albumCarousel": ["immich"], paperlessNgx: ["paperlessNgx"], + patchmon: ["patchmon"], + bazarr: ["bazarr"], tracearr: ["tracearr"], speedtestTracker: ["speedtestTracker"], uptimeKuma: ["uptimeKuma"], audioStats: ["navidrome", "audiobookshelf"], vpn: getIntegrationKindsByCategory("vpn"), + archiveTeamWarrior: ["archiveTeamWarrior"], anchorNote: ["anchor"], openWebUi: ["openWebUi"], + traefik: ["traefik"], }; export const getWidgetKindsForIntegration = (integrationKind: IntegrationKind): WidgetKind[] => { @@ -81,18 +85,7 @@ export const featuredIntegrations: readonly IntegrationKind[] = [ "jellyfin", ]; -export const hiddenFromOnboarding = new Set([ - "searchCh", - "github", - "dockerHub", - "gitlab", - "npm", - "codeberg", - "linuxServerIO", - "gitHubContainerRegistry", - "quay", - "mock", -]); +export const hiddenFromOnboarding = new Set(["mock"]); export const defaultBookmarkApps = [ { diff --git a/packages/definitions/src/widget.ts b/packages/definitions/src/widget.ts index c926fe042..692e5f476 100644 --- a/packages/definitions/src/widget.ts +++ b/packages/definitions/src/widget.ts @@ -17,6 +17,7 @@ export const widgetKinds = [ "mediaRequests-requestList", "mediaRequests-requestStats", "mediaTranscoding", + "mediaMissing", "minecraftServerStatus", "networkControllerSummary", "networkControllerStatus", @@ -36,6 +37,8 @@ export const widgetKinds = [ "immich-serverStats", "immich-albumCarousel", "paperlessNgx", + "patchmon", + "bazarr", "tracearr", "speedtestTracker", "uptimeKuma", @@ -48,6 +51,7 @@ export const widgetKinds = [ "beszelSystemGrid", "beszelAlerts", "beszelSystemStats", + "traefik", "customApi", "openWebUi", ] as const; @@ -57,4 +61,7 @@ export const widgetDefaultSizes: Partial { setQuery(""); }, [value]); @@ -65,13 +63,8 @@ export const IconPicker = ({ const tCommon = useScopedI18n("common"); const [debouncedQuery] = useDebouncedValue(query, 100); - // ponytail: only search when the typed query differs from the committed - // value. On mount the input shows the existing URL (edit mode), which - // would never match the bare filenames in the icon table. const searchText = (debouncedQuery !== (value ?? "") && debouncedQuery) || ""; - // ponytail: not useSuspenseQuery — an above Suspense boundary would - // trigger on every keystroke and the dropdown flash is bad UX. const { data, isLoading, isError, refetch } = clientApi.icon.findIcons.useQuery( { searchText }, { placeholderData: (prev) => prev }, @@ -123,11 +116,8 @@ export const IconPicker = ({ ); }); - // ponytail: show the typed query if non-empty, else fall back to the URL. const inputValue = query || value || ""; - // ponytail: show the existing icon as a left-section preview. const leftSection = shouldShowPreview(value) && ; - // ponytail: prop override -> real count -> loading text. const placeholderText = placeholder || (data && tCommon("iconPicker.header", { countIcons: String(data.countIcons) })) || diff --git a/packages/forms-collection/src/new-app/_form.tsx b/packages/forms-collection/src/new-app/_form.tsx index 2bce21034..92f3a4c2f 100644 --- a/packages/forms-collection/src/new-app/_form.tsx +++ b/packages/forms-collection/src/new-app/_form.tsx @@ -72,7 +72,14 @@ export const AppForm = ({ } form.initialize(toFormValues(initialValues)); - }, [initialValuesKey, form, initialValues]); + // `form` is a new object every render (Mantine useForm returns a fresh literal), + // so it must not be a dep here — it would re-run every render and, since + // form.initialize() always calls clearErrors() (new {} object) internally, + // trigger an infinite update loop. initialValuesKey (a primitive string) + // already captures the only case this effect exists for: re-initializing + // when initialValues actually changes (e.g. navigating between edit pages). + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [initialValuesKey]); // Debounce the name value with 200ms delay const [debouncedName] = useDebouncedValue(form.values.name, 200); diff --git a/packages/integrations/package.json b/packages/integrations/package.json index 6e0c81039..d36be1852 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -27,7 +27,6 @@ "@ctrl/deluge": "catalog:", "@ctrl/qbittorrent": "catalog:", "@ctrl/transmission": "catalog:", - "@gitbeaker/rest": "catalog:", "@homarr/common": "workspace:^0.1.0", "@homarr/core": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0", @@ -39,7 +38,6 @@ "@homarr/validation": "workspace:^0.1.0", "@immich/sdk": "catalog:", "@jellyfin/sdk": "catalog:", - "@octokit/auth-app": "catalog:", "ical.js": "catalog:", "maria2": "catalog:", "octokit": "catalog:", diff --git a/packages/integrations/src/base/creator.ts b/packages/integrations/src/base/creator.ts index c3aaf191b..6af021c03 100644 --- a/packages/integrations/src/base/creator.ts +++ b/packages/integrations/src/base/creator.ts @@ -2,13 +2,11 @@ import type { IntegrationKind } from "@homarr/definitions"; import { AdGuardHomeIntegration } from "../adguard-home/adguard-home-integration"; import { BeszelIntegration } from "../beszel/beszel-integration"; +import { BazarrIntegration } from "../bazarr/bazarr-integration"; import { AnchorIntegration } from "../anchor/anchor-integration"; -import { ArchiveTeamWarriorIntegration } from "../archive-team-warrior/archive-team-warrior-integration"; import { AudiobookshelfIntegration } from "../audiobookshelf/audiobookshelf-integration"; -import { CodebergIntegration } from "../codeberg/codeberg-integration"; import { CoolifyIntegration } from "../coolify/coolify-integration"; import { DashDotIntegration } from "../dashdot/dashdot-integration"; -import { DockerHubIntegration } from "../docker-hub/docker-hub-integration"; import { Aria2Integration } from "../download-client/aria2/aria2-integration"; import { DelugeIntegration } from "../download-client/deluge/deluge-integration"; import { NzbGetIntegration } from "../download-client/nzbget/nzbget-integration"; @@ -17,17 +15,14 @@ import { SabnzbdIntegration } from "../download-client/sabnzbd/sabnzbd-integrati import { SlskdIntegration } from "../download-client/slskd/slskd-integration"; import { TransmissionIntegration } from "../download-client/transmission/transmission-integration"; import { EmbyIntegration } from "../emby/emby-integration"; -import { GitHubContainerRegistryIntegration } from "../github-container-registry/github-container-registry-integration"; -import { GithubIntegration } from "../github/github-integration"; -import { GitlabIntegration } from "../gitlab/gitlab-integration"; import { GlancesIntegration } from "../glances/glances-integration"; +import { ArchiveTeamWarriorIntegration } from "../archive-team-warrior/archive-team-warrior-integration"; import { GluetunIntegration } from "../gluetun/gluetun-integration"; import { HomeAssistantIntegration } from "../homeassistant/homeassistant-integration"; import { ICalIntegration } from "../ical/ical-integration"; import { ImmichIntegration } from "../immich/immich-integration"; import { JellyfinIntegration } from "../jellyfin/jellyfin-integration"; import { JellyseerrIntegration } from "../jellyseerr/jellyseerr-integration"; -import { LinuxServerIOIntegration } from "../linuxserverio/linuxserverio-integration"; import { LidarrIntegration } from "../media-organizer/lidarr/lidarr-integration"; import { RadarrIntegration } from "../media-organizer/radarr/radarr-integration"; import { ReadarrIntegration } from "../media-organizer/readarr/readarr-integration"; @@ -36,7 +31,6 @@ import { TdarrIntegration } from "../media-transcoding/tdarr-integration"; import { MockIntegration } from "../mock/mock-integration"; import { NavidromeIntegration } from "../navidrome/navidrome-integration"; import { NextcloudIntegration } from "../nextcloud/nextcloud.integration"; -import { NPMIntegration } from "../npm/npm-integration"; import { GotifyIntegration } from "../gotify/gotify-integration"; import { OpenWebUiIntegration } from "../open-webui/open-webui-integration"; import { NTFYIntegration } from "../ntfy/ntfy-integration"; @@ -44,17 +38,18 @@ import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-inte import { OPNsenseIntegration } from "../opnsense/opnsense-integration"; import { OverseerrIntegration } from "../overseerr/overseerr-integration"; import { PaperlessNgxIntegration } from "../paperless-ngx/paperless-ngx-integration"; +import { PatchMonIntegration } from "../patchmon/patchmon-integration"; import { PeaNutIntegration } from "../peanut/peanut-integration"; import { createPiHoleIntegrationAsync } from "../pi-hole/pi-hole-integration-factory"; import { createTechnitiumDnsIntegrationAsync } from "../technitium/technitium-integration-factory"; import { PlexIntegration } from "../plex/plex-integration"; import { ProwlarrIntegration } from "../prowlarr/prowlarr-integration"; import { ProxmoxIntegration } from "../proxmox/proxmox-integration"; -import { QuayIntegration } from "../quay/quay-integration"; -import { SearchChIntegration } from "../search-ch/search-ch-integration"; import { SeerrIntegration } from "../seerr/seerr-integration"; import { SpeedtestTrackerIntegration } from "../speedtest-tracker/speedtest-tracker-integration"; import { TracearrIntegration } from "../tracearr/tracearr-integration"; +import { TraefikIntegration } from "../traefik/traefik-integration"; +import { SynologyIntegration } from "../synology/synology-integration"; import { TrueNasIntegration } from "../truenas/truenas-integration"; import { UmamiIntegration } from "../umami/umami-integration"; import { UptimeKumaIntegration } from "../uptime-kuma/uptime-kuma-integration"; @@ -115,26 +110,19 @@ export const integrationCreators = { nextcloud: NextcloudIntegration, unifiController: UnifiControllerIntegration, opnsense: OPNsenseIntegration, - github: GithubIntegration, - dockerHub: DockerHubIntegration, - gitlab: GitlabIntegration, - npm: NPMIntegration, - codeberg: CodebergIntegration, - linuxServerIO: LinuxServerIOIntegration, - gitHubContainerRegistry: GitHubContainerRegistryIntegration, ical: ICalIntegration, - quay: QuayIntegration, ntfy: NTFYIntegration, gotify: GotifyIntegration, mock: MockIntegration, truenas: TrueNasIntegration, + synology: SynologyIntegration, unraid: UnraidIntegration, coolify: CoolifyIntegration, tracearr: TracearrIntegration, glances: GlancesIntegration, - searchCh: SearchChIntegration, immich: ImmichIntegration, paperlessNgx: PaperlessNgxIntegration, + patchmon: PatchMonIntegration, speedtestTracker: SpeedtestTrackerIntegration, audiobookshelf: AudiobookshelfIntegration, navidrome: NavidromeIntegration, @@ -145,6 +133,8 @@ export const integrationCreators = { peaNut: PeaNutIntegration, beszel: BeszelIntegration, openWebUi: OpenWebUiIntegration, + bazarr: BazarrIntegration, + traefik: TraefikIntegration, } satisfies Record Promise]>; type IntegrationInstanceOfKind = { diff --git a/packages/integrations/src/bazarr/bazarr-integration.ts b/packages/integrations/src/bazarr/bazarr-integration.ts new file mode 100644 index 000000000..8f224d6ea --- /dev/null +++ b/packages/integrations/src/bazarr/bazarr-integration.ts @@ -0,0 +1,45 @@ +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; + +import type { IntegrationTestingInput } from "../base/integration"; +import { Integration } from "../base/integration"; +import { TestConnectionError } from "../base/test-connection/test-connection-error"; +import type { TestingResult } from "../base/test-connection/test-connection-service"; +import type { BazarrBadges } from "./bazarr-types"; +import { bazarrBadgesSchema, bazarrSystemStatusSchema } from "./bazarr-types"; + +export class BazarrIntegration extends Integration { + protected async testingAsync(input: IntegrationTestingInput): Promise { + const response = await input.fetchAsync(this.url("/api/system/status"), { + headers: this.getAuthHeaders(), + }); + + if (!response.ok) { + return TestConnectionError.StatusResult(response); + } + + bazarrSystemStatusSchema.parse(await response.json()); + return { success: true }; + } + + public async getBadgesAsync(): Promise { + const response = await fetchWithTrustedCertificatesAsync(this.url("/api/badges"), { + headers: this.getAuthHeaders(), + timeout: 10000, + }); + + if (!response.ok) { + throw new Error( + `Failed to fetch Bazarr badges for ${this.integration.name} (${this.integration.id}): ${response.statusText}`, + ); + } + + return bazarrBadgesSchema.parse(await response.json()); + } + + private getAuthHeaders(): Record { + return { + "X-API-KEY": this.getSecretValue("apiKey"), + Accept: "application/json", + }; + } +} diff --git a/packages/integrations/src/bazarr/bazarr-types.ts b/packages/integrations/src/bazarr/bazarr-types.ts new file mode 100644 index 000000000..fe16dedba --- /dev/null +++ b/packages/integrations/src/bazarr/bazarr-types.ts @@ -0,0 +1,19 @@ +import { z } from "zod/v4"; + +export const bazarrBadgesSchema = z.object({ + episodes: z.number(), + movies: z.number(), + providers: z.number(), + status: z.number(), + sonarr_signalr: z.string(), + radarr_signalr: z.string(), + announcements: z.number(), +}); + +export type BazarrBadges = z.infer; + +export const bazarrSystemStatusSchema = z.object({ + data: z.object({ + bazarr_version: z.string(), + }), +}); diff --git a/packages/integrations/src/beszel/beszel-integration.ts b/packages/integrations/src/beszel/beszel-integration.ts index bf6f3640c..f8c681da3 100644 --- a/packages/integrations/src/beszel/beszel-integration.ts +++ b/packages/integrations/src/beszel/beszel-integration.ts @@ -13,22 +13,72 @@ import type { BeszelAlertHistory, BeszelAuthResponse, BeszelContainer, - BeszelContainerStats, BeszelContainerStatsRecord, BeszelSmartDevice, BeszelSystem, BeszelSystemDetails, BeszelSystemdService, - BeszelSystemStats, BeszelSystemStatsRecord, + BeszelSystemStats, + BeszelContainerStats, CreateAlertInput, + LiveStatsEvent, PocketBaseListResponse, UpdateAlertInput, } from "./beszel-types"; +import { createRealtimeMetricsTopic, PocketBaseSseParser } from "./pocketbase-realtime"; const logger = createLogger({ module: "beszel-integration" }); const escapeFilterValue = (value: string) => value.replace(/\\/g, "\\\\").replace(/'/g, "\\'"); +const realtimeRetryDelaysMs = [200, 300, 500, 1_000, 1_500, 2_000] as const; + +const supportsRealtimeMetrics = (version: string) => { + const [major = 0, minor = 0] = version.replace(/^v/, "").split(".").map(Number); + return major > 0 || minor >= 13; +}; + +const waitForRetryAsync = async (delayMs: number, signal: AbortSignal) => + await new Promise((resolve) => { + if (signal.aborted) return resolve(); + const timeout = setTimeout(done, delayMs); + signal.addEventListener("abort", done, { once: true }); + function done() { + clearTimeout(timeout); + signal.removeEventListener("abort", done); + resolve(); + } + }); + +interface BeszelRealtimeSnapshot { + stats?: BeszelSystemStats; + container?: BeszelContainerStats[]; +} + +export const normalizeRealtimeSnapshot = ( + payload: BeszelRealtimeSnapshot, + systemId: string, + receivedAt = new Date(), +): LiveStatsEvent[] => { + const created = receivedAt.toISOString(); + const id = `realtime-${receivedAt.getTime()}`; + const events: LiveStatsEvent[] = []; + + if (payload.stats && typeof payload.stats === "object" && !Array.isArray(payload.stats)) { + events.push({ + type: "system_stats", + record: { id, system: systemId, stats: payload.stats, type: "1m", created, updated: created }, + }); + } + if (Array.isArray(payload.container)) { + events.push({ + type: "container_stats", + record: { id, system: systemId, stats: payload.container, type: "1m", created, updated: created }, + }); + } + + return events; +}; interface BeszelSession { token: string; @@ -286,109 +336,163 @@ export class BeszelIntegration extends Integration { }); } + /** Subscribe to Beszel's custom one-second `rt_metrics` PocketBase topic. */ public async subscribeRealtimeMetrics( systemId: string, - onMessage: (data: { stats: BeszelSystemStatsRecord; containerStats: BeszelContainerStatsRecord | null }) => void, + onMessage: (event: LiveStatsEvent) => void, signal: AbortSignal, ): Promise { - const session = await this.authenticateAsync(); - const sseUrl = this.url("/api/realtime"); - logger.debug("Opening Beszel SSE connection", { integrationId: this.integration.id, systemId }); - - const sseResponse = await fetchWithTrustedCertificatesAsync(sseUrl, { - headers: { Authorization: session.token }, - signal, - }); - - if (!sseResponse.ok || !sseResponse.body) { - logger.warn("Beszel SSE connection failed", { - integrationId: this.integration.id, - systemId, - status: sseResponse.status, - }); - throw new ResponseError(sseResponse); + const systems = await this.getSystemsAsync(); + const system = systems.find((candidate) => candidate.id === systemId); + if (!system) throw new Error("Selected Beszel system was not found"); + if (system.status !== "up") + throw new Error(`Beszel system is ${system.status}; Live mode requires an online system`); + if (!supportsRealtimeMetrics(system.info.v)) { + throw new Error(`Beszel agent ${system.info.v} does not support Live mode; version 0.13.0 or newer is required`); } - logger.debug("Beszel SSE connection opened", { integrationId: this.integration.id, systemId }); - const reader = sseResponse.body.getReader(); - const decoder = new TextDecoder(); - let buffer = ""; - let clientId: string | null = null; - - const processLine = (line: string) => { - if (!line.startsWith("data:")) return; - const jsonStr = line.slice(5).trim(); - if (!jsonStr) return; + const realtimeUrl = this.url("/api/realtime"); + const topic = createRealtimeMetricsTopic(systemId); + let retryAttempt = 0; + let emittedSnapshotCount = 0; + while (!signal.aborted) { try { - const parsed = JSON.parse(jsonStr) as Record; - - if (!clientId && typeof parsed.clientId === "string") { - clientId = parsed.clientId; - this.subscribeTopic(clientId, systemId, session.token).catch((err) => { - logger.warn("Failed to subscribe to rt_metrics topic, aborting SSE", { error: err }); - reader.cancel().catch(() => {}); + let session = await this.authenticateAsync(); + const openStream = async () => + await fetchWithTrustedCertificatesAsync(realtimeUrl, { + headers: { + Accept: "text/event-stream", + Authorization: session.token, + "Cache-Control": "no-cache", + Pragma: "no-cache", + }, + signal, + bodyTimeout: 0, }); - return; - } - if ("stats" in parsed) { - const now = new Date().toISOString(); - const envelope = { id: "", system: systemId, type: "1m" as const, created: now, updated: now }; - const stats: BeszelSystemStatsRecord = { ...envelope, stats: parsed.stats as BeszelSystemStats }; - - let containerStats: BeszelContainerStatsRecord | null = null; - const hasContainers = - parsed.container && - Array.isArray(parsed.container) && - (parsed.container as BeszelContainerStats[]).length > 0; - if (hasContainers) { - containerStats = { ...envelope, stats: parsed.container as BeszelContainerStats[] }; - } - onMessage({ stats, containerStats }); - } - } catch { - logger.debug("Failed to parse SSE message", { line }); - } - }; - - try { - while (!signal.aborted) { - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split("\n"); - buffer = lines.pop() ?? ""; - for (const line of lines) { - processLine(line); - } - } - if (!signal.aborted) { - logger.warn("Beszel SSE stream ended unexpectedly", { + logger.debug("Opening Beszel rt_metrics SSE connection", { integrationId: this.integration.id, systemId, + retryAttempt, }); - throw new Error("Beszel SSE stream ended unexpectedly"); - } - } finally { - reader.cancel().catch(() => {}); - } - } + let response = await openStream(); + if (response.status === 401) { + await this.sessionStore.clearAsync(); + session = await this.authenticateAsync(); + response = await openStream(); + } + if (!response.ok) throw new ResponseError(response); - private async subscribeTopic(clientId: string, systemId: string, token: string): Promise { - const topic = `rt_metrics?options=${JSON.stringify({ query: { system: systemId } })}`; - const response = await fetchWithTrustedCertificatesAsync(this.url("/api/realtime"), { - method: "POST", - headers: { - Authorization: token, - "Content-Type": "application/json", - }, - body: JSON.stringify({ clientId, subscriptions: [topic] }), - }); - if (!response.ok) { - throw new ResponseError(response); + const reader = response.body?.getReader(); + if (!reader) throw new Error("Beszel realtime response has no body"); + const decoder = new TextDecoder(); + const parser = new PocketBaseSseParser(); + let clientId: string | undefined; + + const processFrameAsync = async (frame: ReturnType[number]) => { + if (frame.event === "PB_CONNECT") { + let fallbackClientId: string | undefined; + try { + const connectData = JSON.parse(frame.data) as { clientId?: string }; + fallbackClientId = connectData.clientId; + } catch { + // PocketBase normally provides the client ID in the SSE id field. + } + clientId = frame.id || fallbackClientId; + if (!clientId) throw new Error("PocketBase PB_CONNECT event did not include a client ID"); + + const subscribeResponse = await fetchWithTrustedCertificatesAsync(realtimeUrl, { + method: "POST", + headers: { Authorization: session.token, "Content-Type": "application/json" }, + body: JSON.stringify({ clientId, subscriptions: [topic] }), + signal, + }); + if (subscribeResponse.status === 401) await this.sessionStore.clearAsync(); + if (!subscribeResponse.ok) throw new ResponseError(subscribeResponse); + logger.debug("Subscribed to Beszel rt_metrics topic", { + integrationId: this.integration.id, + systemId, + clientId, + }); + return; + } + + if (!frame.event.startsWith("rt_metrics")) return; + try { + const snapshot = JSON.parse(frame.data) as BeszelRealtimeSnapshot; + const events = normalizeRealtimeSnapshot(snapshot, systemId); + if (events.length === 0) return; + emittedSnapshotCount += 1; + retryAttempt = 0; + for (const event of events) onMessage(event); + if (emittedSnapshotCount === 1 || emittedSnapshotCount % 30 === 0) { + logger.debug("Forwarded Beszel rt_metrics snapshots", { + integrationId: this.integration.id, + systemId, + emittedSnapshotCount, + containerCount: snapshot.container?.length ?? 0, + }); + } + } catch (error) { + logger.warn("Failed to parse Beszel rt_metrics payload", { + integrationId: this.integration.id, + systemId, + error: error instanceof Error ? error.message : String(error), + }); + } + }; + + logger.debug("Beszel rt_metrics SSE connection established", { + integrationId: this.integration.id, + systemId, + contentType: response.headers.get("content-type"), + }); + try { + while (!signal.aborted) { + const { done, value } = await reader.read(); + if (done) break; + for (const frame of parser.push(decoder.decode(value, { stream: true }))) { + await processFrameAsync(frame); + } + } + for (const frame of parser.push(decoder.decode())) await processFrameAsync(frame); + for (const frame of parser.finish()) await processFrameAsync(frame); + } finally { + await reader.cancel().catch(() => {}); + reader.releaseLock(); + } + + if (!signal.aborted) throw new Error("Beszel rt_metrics SSE stream ended unexpectedly"); + } catch (error) { + if (signal.aborted) break; + if (error instanceof ResponseError && error.statusCode === 401 && retryAttempt === 0) { + retryAttempt += 1; + logger.debug("Retrying Beszel rt_metrics handshake after reauthentication", { + integrationId: this.integration.id, + systemId, + }); + continue; + } + if (error instanceof ResponseError && error.statusCode < 500 && error.statusCode !== 429) throw error; + const delayMs = realtimeRetryDelaysMs[Math.min(retryAttempt, realtimeRetryDelaysMs.length - 1)] ?? 2_000; + retryAttempt += 1; + logger.warn("Beszel rt_metrics connection lost; retrying", { + integrationId: this.integration.id, + systemId, + retryAttempt, + delayMs, + error: error instanceof Error ? error.message : String(error), + }); + await waitForRetryAsync(delayMs, signal); + } } + + logger.debug("Beszel rt_metrics subscription stopped", { + integrationId: this.integration.id, + systemId, + emittedSnapshotCount, + }); } protected async testingAsync(input: IntegrationTestingInput): Promise { diff --git a/packages/integrations/src/beszel/beszel-types.ts b/packages/integrations/src/beszel/beszel-types.ts index e5c6566a5..e8504bb87 100644 --- a/packages/integrations/src/beszel/beszel-types.ts +++ b/packages/integrations/src/beszel/beszel-types.ts @@ -133,9 +133,9 @@ export interface BeszelSystemStats { /** disk usage (%) */ dp: number; /** disk read (MB/s) */ - dr: number; + dr?: number; /** disk write (MB/s) */ - dw: number; + dw?: number; /** disk read max (MB/s) */ drm?: number; /** disk write max (MB/s) */ @@ -145,9 +145,9 @@ export interface BeszelSystemStats { /** disk IOPS max [read, write] */ diom?: [number, number]; /** network sent — all interfaces (bytes/s) */ - ns: number; + ns?: number; /** network received — all interfaces (bytes/s) */ - nr: number; + nr?: number; /** bandwidth — public interfaces only (bytes/s) [sent, recv]. Prefer over ns/nr */ b?: [number, number]; /** network sent max (bytes/s) */ @@ -169,26 +169,26 @@ export interface BeszelSystemStats { } export interface BeszelExtraFsStats { - /** total disk (bytes) */ + /** total disk (GiB) */ d: number; - /** disk used (bytes) */ + /** disk used (GiB) */ du: number; /** read (bytes/s) */ r: number; /** write (bytes/s) */ w: number; /** read max (bytes/s) */ - rm: number; + rm?: number; /** write max (bytes/s) */ - wm: number; + wm?: number; /** read (IOPS) */ - rb: number; + rb?: number; /** write (IOPS) */ - wb: number; + wb?: number; /** read max (IOPS) */ - rbm: number; + rbm?: number; /** write max (IOPS) */ - wbm: number; + wbm?: number; } export interface BeszelGPUData { @@ -343,6 +343,22 @@ export interface UpdateAlertInput { min?: number; } +/** + * Live SSE subscription event types for real-time Beszel metrics. + * Discriminated union — clients accumulate each type into its own buffer. + */ +export interface LiveSystemStatsEvent { + type: "system_stats"; + record: BeszelSystemStatsRecord; +} + +export interface LiveContainerStatsEvent { + type: "container_stats"; + record: BeszelContainerStatsRecord; +} + +export type LiveStatsEvent = LiveSystemStatsEvent | LiveContainerStatsEvent; + export interface PocketBaseListResponse { page: number; perPage: number; @@ -366,6 +382,8 @@ export interface BeszelSystemRow { memory: number; /** disk usage (%) */ disk: number; + /** extra filesystem usage (%) keyed by mount path */ + extraFilesystems: Record; /** GPU usage (%) */ gpu: number; /** load average [1m, 5m, 15m] */ diff --git a/packages/integrations/src/beszel/pocketbase-realtime.ts b/packages/integrations/src/beszel/pocketbase-realtime.ts new file mode 100644 index 000000000..e3330834b --- /dev/null +++ b/packages/integrations/src/beszel/pocketbase-realtime.ts @@ -0,0 +1,65 @@ +export interface PocketBaseSseFrame { + event: string; + id?: string; + data: string; +} + +export const createRealtimeMetricsTopic = (systemId: string) => + `rt_metrics?options=${encodeURIComponent(JSON.stringify({ query: { system: systemId } }))}`; + +export class PocketBaseSseParser { + private buffer = ""; + private event = "message"; + private id: string | undefined; + private dataLines: string[] = []; + + public push(chunk: string): PocketBaseSseFrame[] { + this.buffer += chunk; + const frames: PocketBaseSseFrame[] = []; + const lines = this.buffer.split(/\r?\n/); + this.buffer = lines.pop() ?? ""; + + for (const line of lines) { + const frame = this.processLine(line); + if (frame) frames.push(frame); + } + + return frames; + } + + public finish(): PocketBaseSseFrame[] { + const frames: PocketBaseSseFrame[] = []; + if (this.buffer !== "") { + const frame = this.processLine(this.buffer); + if (frame) frames.push(frame); + this.buffer = ""; + } + const trailingFrame = this.dispatch(); + if (trailingFrame) frames.push(trailingFrame); + return frames; + } + + private processLine(line: string): PocketBaseSseFrame | null { + if (line === "") return this.dispatch(); + if (line.startsWith(":")) return null; + + const colonIndex = line.indexOf(":"); + const field = colonIndex === -1 ? line : line.slice(0, colonIndex); + let value = colonIndex === -1 ? "" : line.slice(colonIndex + 1); + if (value.startsWith(" ")) value = value.slice(1); + + if (field === "event") this.event = value; + else if (field === "id") this.id = value; + else if (field === "data") this.dataLines.push(value); + return null; + } + + private dispatch(): PocketBaseSseFrame | null { + if (this.dataLines.length === 0 && this.event === "message" && this.id === undefined) return null; + const frame = { event: this.event, id: this.id, data: this.dataLines.join("\n") }; + this.event = "message"; + this.id = undefined; + this.dataLines = []; + return frame; + } +} diff --git a/packages/integrations/src/beszel/test/beszel-integration.spec.ts b/packages/integrations/src/beszel/test/beszel-integration.spec.ts index a68e1dbc8..22465fd19 100644 --- a/packages/integrations/src/beszel/test/beszel-integration.spec.ts +++ b/packages/integrations/src/beszel/test/beszel-integration.spec.ts @@ -40,7 +40,124 @@ vi.mock("@homarr/core/infrastructure/certificates", () => ({ getAllTrustedCertificatesAsync: vi.fn().mockResolvedValue([]), })); -import { BeszelIntegration } from "../beszel-integration"; +import { BeszelIntegration, normalizeRealtimeSnapshot } from "../beszel-integration"; +import type { LiveStatsEvent } from "../beszel-types"; +import { createRealtimeMetricsTopic } from "../pocketbase-realtime"; + +describe("normalizeRealtimeSnapshot", () => { + test("normalizes current Beszel host and container snapshots", () => { + const receivedAt = new Date("2026-07-11T13:51:30.217Z"); + const events = normalizeRealtimeSnapshot( + { + stats: { + cpu: 18.07, + m: 31.09, + mu: 16.53, + mp: 53.18, + mb: 14.32, + s: 4, + su: 3.9, + d: 938.8, + du: 455.81, + dp: 51.15, + efs: { sda: { d: 1906.8, du: 1656.45, r: 25.63, w: 0 } }, + }, + container: [{ n: "homarr", c: 1.65, m: 650.56, b: [356066, 19702867] }], + }, + "system-1", + receivedAt, + ); + + expect(events).toHaveLength(2); + expect(events[0]).toMatchObject({ + type: "system_stats", + record: { system: "system-1", created: receivedAt.toISOString(), stats: { cpu: 18.07 } }, + }); + expect(events[1]).toMatchObject({ + type: "container_stats", + record: { system: "system-1", stats: [{ n: "homarr", m: 650.56 }] }, + }); + }); + + test("ignores snapshots without metric data", () => { + expect(normalizeRealtimeSnapshot({}, "system-1")).toEqual([]); + }); +}); + +describe("subscribeRealtimeMetrics", () => { + test("subscribes to Beszel rt_metrics and forwards one-second snapshots", async () => { + const originalFetch = globalThis.fetch; + const topic = createRealtimeMetricsTopic("system-1"); + const subscriptionBodies: unknown[] = []; + const encoder = new TextEncoder(); + + globalThis.fetch = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input); + if (url.endsWith("/api/collections/users/auth-with-password")) { + return Response.json({ token: "test-token", record: { id: "user-1" } }); + } + if (url.includes("/api/collections/systems/records")) { + return Response.json({ + page: 1, + perPage: 500, + totalItems: 1, + totalPages: 1, + items: [ + { + id: "system-1", + name: "Server", + host: "localhost", + port: "45876", + status: "up", + info: { cpu: 1, mp: 2, dp: 3, u: 4, v: "0.18.7" }, + created: "2026-07-11T00:00:00.000Z", + updated: "2026-07-11T00:00:00.000Z", + }, + ], + }); + } + if (url.endsWith("/api/realtime") && init?.method === "POST") { + subscriptionBodies.push(JSON.parse(String(init.body))); + expect(new Headers(init.headers).get("Authorization")).toBe("test-token"); + return new Response(null, { status: 204 }); + } + if (url.endsWith("/api/realtime")) { + const snapshot = JSON.stringify({ + stats: { cpu: 5, m: 10, mu: 4, mp: 40, mb: 1, s: 0, su: 0, d: 20, du: 8, dp: 40 }, + container: [{ n: "homarr", c: 1, m: 256 }], + }); + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode("event: PB_CONNECT\nid: client-1\ndata: {}\n\n")); + controller.enqueue(encoder.encode(`event: ${topic}\ndata: ${snapshot}\n\n`)); + controller.close(); + }, + }); + return new Response(stream, { headers: { "Content-Type": "text/event-stream" } }); + } + throw new Error(`Unexpected request: ${init?.method ?? "GET"} ${url}`); + }) as typeof fetch; + + try { + const integration = createBeszelIntegration(); + const controller = new AbortController(); + const events: LiveStatsEvent[] = []; + await integration.subscribeRealtimeMetrics( + "system-1", + (event) => { + events.push(event); + if (events.length === 2) controller.abort(); + }, + controller.signal, + ); + + expect(subscriptionBodies).toEqual([{ clientId: "client-1", subscriptions: [topic] }]); + expect(events.map((event) => event.type)).toEqual(["system_stats", "container_stats"]); + } finally { + globalThis.fetch = originalFetch; + } + }); +}); function createBeszelIntegration() { return new BeszelIntegration({ diff --git a/packages/integrations/src/beszel/test/pocketbase-realtime.spec.ts b/packages/integrations/src/beszel/test/pocketbase-realtime.spec.ts new file mode 100644 index 000000000..938278d95 --- /dev/null +++ b/packages/integrations/src/beszel/test/pocketbase-realtime.spec.ts @@ -0,0 +1,31 @@ +import { describe, expect, test } from "vitest"; + +import { createRealtimeMetricsTopic, PocketBaseSseParser } from "../pocketbase-realtime"; + +describe("PocketBaseSseParser", () => { + test("parses PB_CONNECT client IDs and CRLF-delimited frames split across chunks", () => { + const parser = new PocketBaseSseParser(); + expect(parser.push("event: PB_CONNECT\r\nid: client-")).toEqual([]); + expect(parser.push("123\r\ndata: {}\r\n\r\n")).toEqual([{ event: "PB_CONNECT", id: "client-123", data: "{}" }]); + }); + + test("joins multiline data and ignores keepalive comments", () => { + const parser = new PocketBaseSseParser(); + expect(parser.push(': keepalive\n\nevent: rt_metrics\ndata: {"stats":\ndata: {}}\n\n')).toEqual([ + { event: "rt_metrics", id: undefined, data: '{"stats":\n{}}' }, + ]); + }); + + test("flushes a trailing frame when the stream ends without a blank line", () => { + const parser = new PocketBaseSseParser(); + expect(parser.push("event: rt_metrics\ndata: {}")).toEqual([]); + expect(parser.finish()).toEqual([{ event: "rt_metrics", id: undefined, data: "{}" }]); + }); +}); + +describe("createRealtimeMetricsTopic", () => { + test("matches the PocketBase custom-topic options encoding used by Beszel", () => { + const topic = createRealtimeMetricsTopic("system-1"); + expect(topic).toBe(`rt_metrics?options=${encodeURIComponent(JSON.stringify({ query: { system: "system-1" } }))}`); + }); +}); diff --git a/packages/integrations/src/codeberg/codeberg-integration.ts b/packages/integrations/src/codeberg/codeberg-integration.ts deleted file mode 100644 index 2396a81e0..000000000 --- a/packages/integrations/src/codeberg/codeberg-integration.ts +++ /dev/null @@ -1,144 +0,0 @@ -import type { RequestInit, Response } from "undici"; - -import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; -import { createLogger } from "@homarr/core/infrastructure/logs"; - -import type { IntegrationTestingInput } from "../base/integration"; -import { Integration } from "../base/integration"; -import { TestConnectionError } from "../base/test-connection/test-connection-error"; -import type { TestingResult } from "../base/test-connection/test-connection-service"; -import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration"; -import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration"; -import type { - DetailsProviderResponse, - ReleaseResponse, -} from "../interfaces/releases-providers/releases-providers-types"; -import { detailsResponseSchema, releasesResponseSchema } from "./codeberg-schemas"; - -const logger = createLogger({ module: "codebergIntegration" }); - -export class CodebergIntegration extends Integration implements ReleasesProviderIntegration { - private async withHeadersAsync(callback: (headers: RequestInit["headers"]) => Promise): Promise { - if (!this.hasSecretValue("personalAccessToken")) return await callback(undefined); - - return await callback({ - Authorization: `token ${this.getSecretValue("personalAccessToken")}`, - }); - } - - protected async testingAsync(input: IntegrationTestingInput): Promise { - const response = await this.withHeadersAsync(async (headers) => { - return await input.fetchAsync(this.url("/version"), { - headers, - }); - }); - - if (!response.ok) { - return TestConnectionError.StatusResult(response); - } - - return { - success: true, - }; - } - - private parseIdentifier(identifier: string) { - const [owner, name] = identifier.split("/"); - if (!owner || !name) { - logger.warn("Invalid identifier format. Expected 'owner/name', for identifier", { - identifier, - }); - return null; - } - return { owner, name }; - } - - public async getLatestMatchingReleaseAsync(identifier: string, versionRegex?: string): Promise { - const parsedIdentifier = this.parseIdentifier(identifier); - if (!parsedIdentifier) return { success: false, error: { code: "invalidIdentifier" } }; - - const { owner, name } = parsedIdentifier; - - const releasesResponse = await this.withHeadersAsync(async (headers) => { - return await fetchWithTrustedCertificatesAsync( - this.url(`/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/releases`), - { headers }, - ); - }); - if (!releasesResponse.ok) { - return { success: false, error: { code: "unexpected", message: releasesResponse.statusText } }; - } - - const releasesResponseJson: unknown = await releasesResponse.json(); - const { data, success, error } = releasesResponseSchema.safeParse(releasesResponseJson); - if (!success) { - return { - success: false, - error: { - code: "unexpected", - message: releasesResponseJson ? JSON.stringify(releasesResponseJson, null, 2) : error.message, - }, - }; - } - - const formattedReleases = data.map((tag) => ({ - latestRelease: tag.tag_name, - latestReleaseAt: tag.published_at, - releaseUrl: tag.url, - releaseDescription: tag.body, - isPreRelease: tag.prerelease, - })); - - const latestRelease = getLatestRelease(formattedReleases, versionRegex); - if (!latestRelease) return { success: false, error: { code: "noMatchingVersion" } }; - - const details = await this.getDetailsAsync(owner, name); - - return { success: true, data: { ...details, ...latestRelease } }; - } - - protected async getDetailsAsync(owner: string, name: string): Promise { - const response = await this.withHeadersAsync(async (headers) => { - return await fetchWithTrustedCertificatesAsync( - this.url(`/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`), - { - headers, - }, - ); - }); - - if (!response.ok) { - logger.warn("Failed to get details", { - owner, - name, - error: response.statusText, - }); - - return undefined; - } - - const responseJson = await response.json(); - const { data, success, error } = detailsResponseSchema.safeParse(responseJson); - - if (!success) { - logger.warn("Failed to parse details", { - owner, - name, - error, - }); - - return undefined; - } - - return { - projectUrl: data.html_url, - projectDescription: data.description, - isFork: data.fork, - isArchived: data.archived, - createdAt: data.created_at, - starsCount: data.stars_count, - openIssues: data.open_issues_count, - forksCount: data.forks_count, - }; - } -} diff --git a/packages/integrations/src/codeberg/codeberg-schemas.ts b/packages/integrations/src/codeberg/codeberg-schemas.ts deleted file mode 100644 index ff6e62a01..000000000 --- a/packages/integrations/src/codeberg/codeberg-schemas.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { z } from "zod/v4"; - -export const releasesResponseSchema = z.array( - z.object({ - tag_name: z.string(), - published_at: z.string().transform((value) => new Date(value)), - url: z.string(), - body: z.string(), - prerelease: z.boolean(), - }), -); - -export const detailsResponseSchema = z.object({ - html_url: z.string(), - description: z.string(), - fork: z.boolean(), - archived: z.boolean(), - created_at: z.string().transform((value) => new Date(value)), - stars_count: z.number(), - open_issues_count: z.number(), - forks_count: z.number(), -}); diff --git a/packages/integrations/src/docker-hub/docker-hub-integration.ts b/packages/integrations/src/docker-hub/docker-hub-integration.ts deleted file mode 100644 index b9e9ea09c..000000000 --- a/packages/integrations/src/docker-hub/docker-hub-integration.ts +++ /dev/null @@ -1,190 +0,0 @@ -import type { fetch, RequestInit, Response } from "undici"; - -import { ResponseError } from "@homarr/common/server"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; -import { createLogger } from "@homarr/core/infrastructure/logs"; - -import type { IntegrationInput, IntegrationTestingInput } from "../base/integration"; -import { Integration } from "../base/integration"; -import type { SessionStore } from "../base/session-store"; -import { createSessionStore } from "../base/session-store"; -import { TestConnectionError } from "../base/test-connection/test-connection-error"; -import type { TestingResult } from "../base/test-connection/test-connection-service"; -import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration"; -import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration"; -import type { - DetailsProviderResponse, - ReleaseResponse, -} from "../interfaces/releases-providers/releases-providers-types"; -import { accessTokenResponseSchema, detailsResponseSchema, releasesResponseSchema } from "./docker-hub-schemas"; - -const logger = createLogger({ module: "dockerHubIntegration" }); - -export class DockerHubIntegration extends Integration implements ReleasesProviderIntegration { - private readonly sessionStore: SessionStore; - - constructor(integration: IntegrationInput) { - super(integration); - this.sessionStore = createSessionStore(integration); - } - - private async withHeadersAsync(callback: (headers: RequestInit["headers"]) => Promise): Promise { - if (!this.hasSecretValue("username") || !this.hasSecretValue("personalAccessToken")) - return await callback(undefined); - - const storedSession = await this.sessionStore.getAsync(); - - if (storedSession) { - logger.debug("Using stored session for request", { integrationId: this.integration.id }); - const response = await callback({ - Authorization: `Bearer ${storedSession}`, - }); - if (response.status !== 401) { - return response; - } - - logger.debug("Session expired, getting new session", { integrationId: this.integration.id }); - } - - const accessToken = await this.getSessionAsync(); - await this.sessionStore.setAsync(accessToken); - return await callback({ - Authorization: `Bearer ${accessToken}`, - }); - } - - protected async testingAsync(input: IntegrationTestingInput): Promise { - const hasAuth = this.hasSecretValue("username") && this.hasSecretValue("personalAccessToken"); - - if (hasAuth) { - logger.debug("Testing DockerHub connection with authentication", { integrationId: this.integration.id }); - await this.getSessionAsync(input.fetchAsync); - } else { - logger.debug("Testing DockerHub connection without authentication", { integrationId: this.integration.id }); - const response = await input.fetchAsync(this.url("/v2/repositories/library")); - if (!response.ok) { - return TestConnectionError.StatusResult(response); - } - } - - return { - success: true, - }; - } - - private parseIdentifier(identifier: string) { - if (!identifier.includes("/")) return { owner: "", name: identifier }; - const [owner, name] = identifier.split("/"); - if (!owner || !name) { - logger.warn("Invalid identifier format. Expected 'owner/name' or 'name', for identifier", { - identifier, - }); - return null; - } - return { owner, name }; - } - - public async getLatestMatchingReleaseAsync(identifier: string, versionRegex?: string): Promise { - const parsedIdentifier = this.parseIdentifier(identifier); - if (!parsedIdentifier) return { success: false, error: { code: "invalidIdentifier" } }; - - const { owner, name } = parsedIdentifier; - - const relativeUrl: `/${string}` = owner - ? `/v2/namespaces/${encodeURIComponent(owner)}/repositories/${encodeURIComponent(name)}` - : `/v2/repositories/library/${encodeURIComponent(name)}`; - - for (let page = 0; page <= 5; page++) { - const releasesResponse = await this.withHeadersAsync(async (headers) => { - return await fetchWithTrustedCertificatesAsync(this.url(`${relativeUrl}/tags?page_size=100&page=${page}`), { - headers, - }); - }); - if (!releasesResponse.ok) { - return { success: false, error: { code: "unexpected", message: releasesResponse.statusText } }; - } - - const releasesResponseJson: unknown = await releasesResponse.json(); - const releasesResult = releasesResponseSchema.safeParse(releasesResponseJson); - if (!releasesResult.success) { - return { - success: false, - error: { - code: "unexpected", - message: releasesResponseJson - ? JSON.stringify(releasesResponseJson, null, 2) - : releasesResult.error.message, - }, - }; - } - - const latestRelease = getLatestRelease(releasesResult.data.results, versionRegex); - if (!latestRelease) continue; - - const details = await this.getDetailsAsync(relativeUrl); - - return { success: true, data: { ...details, ...latestRelease } }; - } - - return { success: false, error: { code: "noMatchingVersion" } }; - } - - private async getDetailsAsync(relativeUrl: `/${string}`): Promise { - const response = await this.withHeadersAsync(async (headers) => { - return await fetchWithTrustedCertificatesAsync(this.url(`${relativeUrl}/`), { - headers, - }); - }); - - if (!response.ok) { - logger.warn("Failed to get details response", { - relativeUrl, - error: response.statusText, - }); - - return undefined; - } - - const responseJson = await response.json(); - const { data, success, error } = detailsResponseSchema.safeParse(responseJson); - - if (!success) { - logger.warn("Failed to parse details response", { - relativeUrl, - error, - }); - - return undefined; - } - - return { - projectUrl: `https://hub.docker.com/r/${data.namespace === "library" ? "_" : data.namespace}/${data.name}`, - projectDescription: data.description, - createdAt: data.date_registered, - starsCount: data.star_count, - }; - } - - private async getSessionAsync(fetchAsync: typeof fetch = fetchWithTrustedCertificatesAsync): Promise { - const response = await fetchAsync(this.url("/v2/auth/token"), { - method: "POST", - body: JSON.stringify({ - identifier: this.getSecretValue("username"), - secret: this.getSecretValue("personalAccessToken"), - }), - }); - - if (!response.ok) throw new ResponseError(response); - - const data = await response.json(); - const result = await accessTokenResponseSchema.parseAsync(data); - - if (!result.access_token) { - throw new ResponseError({ status: 401, url: response.url }); - } - - logger.info("Received session successfully", { integrationId: this.integration.id }); - - return result.access_token; - } -} diff --git a/packages/integrations/src/docker-hub/docker-hub-schemas.ts b/packages/integrations/src/docker-hub/docker-hub-schemas.ts deleted file mode 100644 index 674ae5532..000000000 --- a/packages/integrations/src/docker-hub/docker-hub-schemas.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { z } from "zod/v4"; - -export const accessTokenResponseSchema = z.object({ - access_token: z.string(), -}); - -export const releasesResponseSchema = z.object({ - results: z.array( - z.object({ name: z.string(), last_updated: z.string().transform((value) => new Date(value)) }).transform((tag) => ({ - latestRelease: tag.name, - latestReleaseAt: tag.last_updated, - })), - ), -}); - -export const detailsResponseSchema = z.object({ - name: z.string(), - namespace: z.string(), - description: z.string(), - star_count: z.number(), - date_registered: z.string().transform((value) => new Date(value)), -}); diff --git a/packages/integrations/src/download-client/qbittorrent/qbittorrent-integration.ts b/packages/integrations/src/download-client/qbittorrent/qbittorrent-integration.ts index a142fb1a3..c08dd5848 100644 --- a/packages/integrations/src/download-client/qbittorrent/qbittorrent-integration.ts +++ b/packages/integrations/src/download-client/qbittorrent/qbittorrent-integration.ts @@ -99,11 +99,14 @@ export class QBitTorrentIntegration extends Integration implements IDownloadClie password: this.getSecretValue("password"), }; - return new QBittorrent({ + const client = new QBittorrent({ ...credentials, baseUrl: this.url("/").toString(), dispatcher: dispatcher ?? (await createCertificateAgentAsync()), }); + // Populate version state so the library uses correct v5 endpoints (stop/start vs pause/resume) + await client.getAppVersion(); + return client; } private static getTorrentState(state: string): DownloadClientItem["state"] { @@ -125,6 +128,8 @@ export class QBitTorrentIntegration extends Integration implements IDownloadClie return "seeding"; case "pausedDL": case "pausedUP": + case "stoppedDL": + case "stoppedUP": return "paused"; case "stalledDL": return "stalled"; diff --git a/packages/integrations/src/github-container-registry/github-container-registry-integration.ts b/packages/integrations/src/github-container-registry/github-container-registry-integration.ts deleted file mode 100644 index 1400551a8..000000000 --- a/packages/integrations/src/github-container-registry/github-container-registry-integration.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { createAppAuth } from "@octokit/auth-app"; -import { Octokit, RequestError } from "octokit"; -import type { fetch } from "undici"; - -import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; -import { createLogger } from "@homarr/core/infrastructure/logs"; - -import { HandleIntegrationErrors } from "../base/errors/decorator"; -import { integrationOctokitHttpErrorHandler } from "../base/errors/http"; -import type { IntegrationTestingInput } from "../base/integration"; -import { Integration } from "../base/integration"; -import type { TestingResult } from "../base/test-connection/test-connection-service"; -import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration"; -import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration"; -import type { - DetailsProviderResponse, - ReleaseProviderResponse, - ReleaseResponse, -} from "../interfaces/releases-providers/releases-providers-types"; - -const logger = createLogger({ module: "githubContainerRegistryIntegration" }); - -@HandleIntegrationErrors([integrationOctokitHttpErrorHandler]) -export class GitHubContainerRegistryIntegration extends Integration implements ReleasesProviderIntegration { - private static readonly userAgent = "Homarr-Lab/Homarr:GitHubContainerRegistryIntegration"; - - protected async testingAsync(input: IntegrationTestingInput): Promise { - const api = this.getApi(input.fetchAsync); - - if (this.hasSecretValue("personalAccessToken")) { - await api.rest.users.getAuthenticated(); - } else if (this.hasSecretValue("githubAppId")) { - await api.rest.apps.getInstallation({ - installation_id: Number(this.getSecretValue("githubInstallationId")), - }); - } else { - await api.request("GET /octocat"); - } - - return { - success: true, - }; - } - - private parseIdentifier(identifier: string) { - const [owner, name] = identifier.split("/"); - if (!owner || !name) { - logger.warn("Invalid identifier format. Expected 'owner/name', for identifier", { identifier }); - return null; - } - return { owner, name }; - } - - public async getLatestMatchingReleaseAsync(identifier: string, versionRegex?: string): Promise { - const parsedIdentifier = this.parseIdentifier(identifier); - if (!parsedIdentifier) return { success: false, error: { code: "invalidIdentifier" } }; - - const { owner, name } = parsedIdentifier; - const api = this.getApi(); - - try { - const releasesResponse = await api.rest.packages.getAllPackageVersionsForPackageOwnedByUser({ - username: owner, - package_type: "container", - package_name: name, - per_page: 100, - }); - - const releasesProviderResponse = releasesResponse.data.reduce((acc, release) => { - if (!release.metadata?.container?.tags || !(release.metadata.container.tags.length > 0)) return acc; - - release.metadata.container.tags.forEach((tag) => { - acc.push({ - latestRelease: tag, - latestReleaseAt: new Date(release.updated_at), - releaseUrl: release.html_url, - releaseDescription: release.description ?? undefined, - }); - }); - return acc; - }, []); - - const latestRelease = getLatestRelease(releasesProviderResponse, versionRegex); - if (!latestRelease) return { success: false, error: { code: "noMatchingVersion" } }; - - const details = await this.getDetailsAsync(api, owner, name); - - return { success: true, data: { ...details, ...latestRelease } }; - } catch (error) { - const errorMessage = error instanceof RequestError ? error.message : String(error); - logger.warn("Failed to get releases", { - owner, - name, - error: errorMessage, - }); - return { success: false, error: { code: "unexpected", message: errorMessage } }; - } - } - - protected async getDetailsAsync( - api: Octokit, - owner: string, - name: string, - ): Promise { - try { - const response = await api.rest.packages.getPackageForUser({ - username: owner, - package_type: "container", - package_name: name, - }); - - return { - projectUrl: response.data.repository?.html_url ?? response.data.html_url, - projectDescription: response.data.repository?.description ?? undefined, - isFork: response.data.repository?.fork, - isArchived: response.data.repository?.archived, - createdAt: new Date(response.data.created_at), - starsCount: response.data.repository?.stargazers_count, - openIssues: response.data.repository?.open_issues_count, - forksCount: response.data.repository?.forks_count, - }; - } catch (error) { - logger.warn("Failed to get details", { - owner, - name, - error: error instanceof RequestError ? error.message : String(error), - }); - return undefined; - } - } - - private getAuthProperties(): Pick { - if (this.hasSecretValue("personalAccessToken")) - return { - auth: this.getSecretValue("personalAccessToken"), - }; - - if (this.hasSecretValue("githubAppId")) - return { - authStrategy: createAppAuth, - auth: { - appId: this.getSecretValue("githubAppId"), - installationId: this.getSecretValue("githubInstallationId"), - privateKey: this.getSecretValue("privateKey"), - } satisfies Parameters[0], - }; - - return {}; - } - - private getApi(customFetch?: typeof fetch) { - return new Octokit({ - baseUrl: this.url("/").origin, - request: { - fetch: customFetch ?? fetchWithTrustedCertificatesAsync, - }, - userAgent: GitHubContainerRegistryIntegration.userAgent, - // Disable throttling for this integration, Octokit will retry by default after a set time, - // thus delaying the repsonse to the user in case of errors. Errors will be shown to the user, no need to retry the request. - throttle: { enabled: false }, - ...this.getAuthProperties(), - }); - } -} - -type OctokitOptions = Exclude[0], undefined>; diff --git a/packages/integrations/src/github/github-integration.ts b/packages/integrations/src/github/github-integration.ts deleted file mode 100644 index 01548ecc6..000000000 --- a/packages/integrations/src/github/github-integration.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { createAppAuth } from "@octokit/auth-app"; -import { Octokit, RequestError as OctokitRequestError } from "octokit"; -import type { fetch } from "undici"; - -import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; -import { createLogger } from "@homarr/core/infrastructure/logs"; - -import { HandleIntegrationErrors } from "../base/errors/decorator"; -import { integrationOctokitHttpErrorHandler } from "../base/errors/http"; -import type { IntegrationTestingInput } from "../base/integration"; -import { Integration } from "../base/integration"; -import type { TestingResult } from "../base/test-connection/test-connection-service"; -import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration"; -import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration"; -import type { - DetailsProviderResponse, - ReleaseProviderResponse, - ReleaseResponse, -} from "../interfaces/releases-providers/releases-providers-types"; - -const logger = createLogger({ module: "githubIntegration" }); - -@HandleIntegrationErrors([integrationOctokitHttpErrorHandler]) -export class GithubIntegration extends Integration implements ReleasesProviderIntegration { - private static readonly userAgent = "Homarr-Lab/Homarr:GithubIntegration"; - - protected async testingAsync(input: IntegrationTestingInput): Promise { - const api = this.getApi(input.fetchAsync); - - if (this.hasSecretValue("personalAccessToken")) { - await api.rest.users.getAuthenticated(); - } else if (this.hasSecretValue("githubAppId")) { - await api.rest.apps.getInstallation({ - installation_id: Number(this.getSecretValue("githubInstallationId")), - }); - } else { - await api.request("GET /octocat"); - } - - return { - success: true, - }; - } - - private parseIdentifier(identifier: string) { - const [owner, name] = identifier.split("/"); - if (!owner || !name) { - logger.warn("Invalid identifier format. Expected 'owner/name' for identifier", { - identifier, - }); - return null; - } - return { owner, name }; - } - - public async getLatestMatchingReleaseAsync(identifier: string, versionRegex?: string): Promise { - const parsedIdentifier = this.parseIdentifier(identifier); - if (!parsedIdentifier) return { success: false, error: { code: "invalidIdentifier" } }; - - const { owner, name } = parsedIdentifier; - const api = this.getApi(); - - try { - const releasesResponse = await api.rest.repos.listReleases({ owner, repo: name }); - - if (releasesResponse.data.length === 0) { - logger.warn("No releases found", { - identifier: `${owner}/${name}`, - }); - return { success: false, error: { code: "noMatchingVersion" } }; - } - - const releasesProviderResponse = releasesResponse.data.reduce((acc, release) => { - if (!release.published_at) return acc; - - acc.push({ - latestRelease: release.tag_name, - latestReleaseAt: new Date(release.published_at), - releaseUrl: release.html_url, - releaseDescription: release.body ?? undefined, - isPreRelease: release.prerelease, - }); - return acc; - }, []); - - const latestRelease = getLatestRelease(releasesProviderResponse, versionRegex); - if (!latestRelease) return { success: false, error: { code: "noMatchingVersion" } }; - - const details = await this.getDetailsAsync(api, owner, name); - - return { success: true, data: { ...details, ...latestRelease } }; - } catch (error) { - const errorMessage = error instanceof OctokitRequestError ? error.message : String(error); - logger.warn("Failed to get releases", { - owner, - name, - error: errorMessage, - }); - return { success: false, error: { code: "unexpected", message: errorMessage } }; - } - } - - protected async getDetailsAsync( - api: Octokit, - owner: string, - name: string, - ): Promise { - try { - const response = await api.rest.repos.get({ - owner, - repo: name, - }); - - return { - projectUrl: response.data.html_url, - projectDescription: response.data.description ?? undefined, - isFork: response.data.fork, - isArchived: response.data.archived, - createdAt: new Date(response.data.created_at), - starsCount: response.data.stargazers_count, - openIssues: response.data.open_issues_count, - forksCount: response.data.forks_count, - }; - } catch (error) { - logger.warn("Failed to get details", { - owner, - name, - error: error instanceof OctokitRequestError ? error.message : String(error), - }); - return undefined; - } - } - - private getAuthProperties(): Pick { - if (this.hasSecretValue("personalAccessToken")) - return { - auth: this.getSecretValue("personalAccessToken"), - }; - - if (this.hasSecretValue("githubAppId")) - return { - authStrategy: createAppAuth, - auth: { - appId: this.getSecretValue("githubAppId"), - installationId: this.getSecretValue("githubInstallationId"), - privateKey: this.getSecretValue("privateKey"), - } satisfies Parameters[0], - }; - - return {}; - } - - private getApi(customFetch?: typeof fetch) { - return new Octokit({ - baseUrl: this.url("/").origin, - request: { - fetch: customFetch ?? fetchWithTrustedCertificatesAsync, - }, - userAgent: GithubIntegration.userAgent, - // Disable throttling for this integration, Octokit will retry by default after a set time, - // thus delaying the repsonse to the user in case of errors. Errors will be shown to the user, no need to retry the request. - throttle: { enabled: false }, - ...this.getAuthProperties(), - }); - } -} - -type OctokitOptions = Exclude[0], undefined>; diff --git a/packages/integrations/src/gitlab/gitlab-integration.ts b/packages/integrations/src/gitlab/gitlab-integration.ts deleted file mode 100644 index fd1ebc926..000000000 --- a/packages/integrations/src/gitlab/gitlab-integration.ts +++ /dev/null @@ -1,155 +0,0 @@ -import type { Gitlab as CoreGitlab } from "@gitbeaker/core"; -import { createRequesterFn, defaultOptionsHandler } from "@gitbeaker/requester-utils"; -import type { FormattedResponse, RequestOptions, ResourceOptions } from "@gitbeaker/requester-utils"; -import { Gitlab } from "@gitbeaker/rest"; - -import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; -import { createLogger } from "@homarr/core/infrastructure/logs"; - -import type { IntegrationTestingInput } from "../base/integration"; -import { Integration } from "../base/integration"; -import { TestConnectionError } from "../base/test-connection/test-connection-error"; -import type { TestingResult } from "../base/test-connection/test-connection-service"; -import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration"; -import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration"; -import type { - DetailsProviderResponse, - ReleaseProviderResponse, - ReleaseResponse, -} from "../interfaces/releases-providers/releases-providers-types"; - -const logger = createLogger({ module: "gitlabIntegration" }); - -export class GitlabIntegration extends Integration implements ReleasesProviderIntegration { - protected async testingAsync(input: IntegrationTestingInput): Promise { - const response = await input.fetchAsync(this.url("/api/v4/projects"), { - headers: { - ...(this.hasSecretValue("personalAccessToken") - ? { Authorization: `Bearer ${this.getSecretValue("personalAccessToken")}` } - : {}), - }, - }); - - if (!response.ok) { - return TestConnectionError.StatusResult(response); - } - - return { - success: true, - }; - } - - public async getLatestMatchingReleaseAsync(identifier: string, versionRegex?: string): Promise { - const api = this.getApi(); - - try { - const releasesResponse = await api.ProjectReleases.all(identifier, { - perPage: 100, - }); - - if (releasesResponse instanceof Error) { - logger.warn("No releases found", { - identifier, - error: releasesResponse.message, - }); - return { success: false, error: { code: "noReleasesFound" } }; - } - - const releasesProviderResponse = releasesResponse.reduce((acc, release) => { - if (!release.released_at) return acc; - - const releaseDate = new Date(release.released_at); - - acc.push({ - latestRelease: release.name ?? release.tag_name, - latestReleaseAt: releaseDate, - releaseUrl: release._links.self, - releaseDescription: release.description ?? undefined, - isPreRelease: releaseDate > new Date(), // For upcoming releases the `released_at` will be set to the future (https://docs.gitlab.com/api/releases/#upcoming-releases). Gitbreaker doesn't currently support the `upcoming_release` field (https://github.com/jdalrymple/gitbeaker/issues/3730) - }); - return acc; - }, []); - - const latestRelease = getLatestRelease(releasesProviderResponse, versionRegex); - if (!latestRelease) return { success: false, error: { code: "noMatchingVersion" } }; - - const details = await this.getDetailsAsync(api, identifier); - - return { success: true, data: { ...details, ...latestRelease } }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - logger.warn("Failed to get releases", { - identifier, - error: errorMessage, - }); - return { success: false, error: { code: "unexpected", message: errorMessage } }; - } - } - - protected async getDetailsAsync(api: CoreGitlab, identifier: string): Promise { - try { - const response = await api.Projects.show(identifier); - - if (response instanceof Error) { - logger.warn("Failed to get details", { - identifier, - error: response.message, - }); - - return undefined; - } - - if (!response.web_url) { - logger.warn("No web URL found", { - identifier, - }); - return undefined; - } - - return { - projectUrl: response.web_url, - projectDescription: response.description ?? undefined, - isFork: response.forked_from_project !== null, - isArchived: response.archived, - createdAt: new Date(response.created_at), - starsCount: response.star_count, - openIssues: response.open_issues_count, - forksCount: response.forks_count, - }; - } catch (error) { - logger.warn("Failed to get details", { - identifier, - error: error instanceof Error ? error.message : String(error), - }); - - return undefined; - } - } - - private getApi() { - return new Gitlab({ - host: this.url("/").origin, - requesterFn: createRequesterFn( - async (serviceOptions: ResourceOptions, _: RequestOptions) => await defaultOptionsHandler(serviceOptions), - async (endpoint: string, options?: Record): Promise => { - if (options === undefined) { - throw new Error("Gitlab library is not configured correctly. Options must be provided."); - } - - const response = await fetchWithTrustedCertificatesAsync( - `${options.prefixUrl as string}${endpoint}`, - options, - ); - const headers = Object.fromEntries(response.headers.entries()); - - return { - status: response.status, - headers, - body: await response.json(), - } as FormattedResponse; - }, - ), - ...(this.hasSecretValue("personalAccessToken") ? { token: this.getSecretValue("personalAccessToken") } : {}), - }); - } -} diff --git a/packages/integrations/src/glances/glances-integration.ts b/packages/integrations/src/glances/glances-integration.ts index 6364ebcb7..5915b44b2 100644 --- a/packages/integrations/src/glances/glances-integration.ts +++ b/packages/integrations/src/glances/glances-integration.ts @@ -35,7 +35,7 @@ export class GlancesIntegration extends Integration implements ISystemHealthMoni if (session == null) throw new Error("Session was unexpectitly null"); - const stats = await this.getAllStatsAsync(); + const [stats, cpuTemp] = await Promise.all([this.getAllStatsAsync(), this.getCpuTempAsync()]); return { cpuUtilization: stats.cpu.total, @@ -58,7 +58,7 @@ export class GlancesIntegration extends Integration implements ISystemHealthMoni cpuModelName: stats.quicklook?.cpu_name ?? "Unknown", loadAverage: null, smart: [], - cpuTemp: undefined, + cpuTemp, gpu: stats.gpu.map((gpu) => ({ gpuId: gpu.gpu_id, name: gpu.name, @@ -80,6 +80,23 @@ export class GlancesIntegration extends Integration implements ISystemHealthMoni return await response.text(); } + private async getCpuTempAsync(): Promise { + // CPU temperature is optional data, so a transport error or payload drift on + // /api/4/sensors must not fail the whole getSystemInfoAsync() call. + try { + const response = await fetchWithTrustedCertificatesAsync(this.url("/api/4/sensors")); + + if (!response.ok) { + return undefined; + } + + const sensors = await sensorsSchema.parseAsync(await response.json()); + return parseGlancesCpuTempFromSensors(sensors); + } catch { + return undefined; + } + } + private async getAllStatsAsync() { const response = await fetchWithTrustedCertificatesAsync(this.url("/api/4/all")); @@ -173,3 +190,33 @@ const allSchema = z.object({ ) .default([]), }); + +const sensorsSchema = z.array( + z.object({ + label: z.string(), + value: z.number(), + type: z.string().optional(), + }), +); + +const cpuTempLabelPriority = ["CPU", "Package id 0"] as const; + +export const parseGlancesCpuTempFromSensors = (sensors: z.infer): number | undefined => { + const temperatureSensors = sensors.filter( + (sensor) => sensor.type === "temperature_core" || sensor.type === "temperature", + ); + + for (const label of cpuTempLabelPriority) { + const match = temperatureSensors.find((sensor) => sensor.label === label); + if (match) { + return match.value; + } + } + + const firstCore = temperatureSensors[0]; + if (firstCore) { + return firstCore.value; + } + + return undefined; +}; diff --git a/packages/integrations/src/gluetun/gluetun-types.ts b/packages/integrations/src/gluetun/gluetun-types.ts index 6886d72a7..d9fdc626a 100644 --- a/packages/integrations/src/gluetun/gluetun-types.ts +++ b/packages/integrations/src/gluetun/gluetun-types.ts @@ -46,7 +46,7 @@ const gluetunServerSelectionSchema = z.object({ isps: z.array(z.string()).nullable(), names: z.array(z.string()).nullable(), numbers: z.array(z.number()).nullable(), - hostnames: z.array(z.string()), + hostnames: z.array(z.string()).nullable(), owned_only: z.boolean(), free_only: z.boolean(), premium_only: z.boolean(), @@ -65,7 +65,7 @@ const gluetunPortForwardingSchema = z.object({ status_file_path: z.string(), up_command: z.string(), down_command: z.string(), - listening_port: z.number(), + listening_port: z.union([z.number(), z.array(z.number())]), username: z.string(), password: z.string(), }); @@ -98,7 +98,7 @@ const gluetunOpenvpnSettingsSchema = z.object({ const gluetunWireguardSettingsSchema = z.object({ private_key: z.string(), pre_shared_key: z.string(), - addresses: z.array(z.string()), + addresses: z.array(z.string()).nullable(), allowed_ips: z.array(z.string()), interface: z.string(), persistent_keep_alive_interval: z.number(), diff --git a/packages/integrations/src/gluetun/test/gluetun-integration.spec.ts b/packages/integrations/src/gluetun/test/gluetun-integration.spec.ts index 1f84005f3..0ff870fad 100644 --- a/packages/integrations/src/gluetun/test/gluetun-integration.spec.ts +++ b/packages/integrations/src/gluetun/test/gluetun-integration.spec.ts @@ -124,6 +124,126 @@ const VPN_SETTINGS_PAYLOAD = { }, }; +const ALT_VPN_SETTINGS_PAYLOAD = { + type: "openvpn", + provider: { + name: "custom", + server_selection: { + vpn: "openvpn", + countries: null, + categories: null, + regions: null, + cities: null, + isps: null, + names: null, + numbers: null, + hostnames: null, + owned_only: false, + free_only: false, + premium_only: false, + stream_only: false, + multi_hop_only: false, + port_forward_only: false, + secure_core_only: false, + tor_only: false, + openvpn: { + config_file_path: "/gluetun/custom.conf", + protocol: "tcp", + endpoint_ip: "0.0.0.0", + custom_port: 0, + pia_encryption_preset: "", + }, + wireguard: { + endpoint_ip: "0.0.0.0", + endpoint_port: 0, + public_key: "", + }, + }, + port_forwarding: { + enabled: false, + provider: "", + status_file_path: "/tmp/gluetun/forwarded_port", + up_command: "", + down_command: "", + listening_port: [0], + ports_count: 0, + username: "", + password: "", + }, + }, + amneziawg: { + wireguard: { + private_key: "", + pre_shared_key: "", + addresses: null, + allowed_ips: ["0.0.0.0/0", "::/0"], + interface: "tun0", + persistent_keep_alive_interval: 0, + mtu: 0, + implementation: "userspace", + }, + junk_packet_count: 0, + junk_packet_min: 0, + junk_packet_max: 0, + padding_s1: 0, + padding_s2: 0, + padding_s3: 0, + padding_s4: 0, + header_h1: "", + header_h2: "", + header_h3: "", + header_h4: "", + init_packet_i1: "", + init_packet_i2: "", + init_packet_i3: "", + init_packet_i4: "", + init_packet_i5: "", + }, + openvpn: { + version: "2.6", + user: "", + password: "", + config_file_path: "/gluetun/custom.conf", + ciphers: null, + auth: "", + cert: "", + key: "", + encrypted_key: "", + key_passphrase: "", + pia_encryption_preset: "", + mssfix: 0, + interface: "tun0", + process_user: "nonrootuser", + verbosity: 1, + flags: null, + }, + wireguard: { + private_key: "", + pre_shared_key: "", + addresses: null, + allowed_ips: ["0.0.0.0/0", "::/0"], + interface: "tun0", + persistent_keep_alive_interval: 0, + mtu: 0, + implementation: "auto", + }, + pmtud: { + icmp_addresses: ["1.1.1.1", "8.8.8.8"], + tcp_addresses: [ + "1.1.1.1:443", + "8.8.8.8:443", + "1.1.1.1:53", + "8.8.8.8:53", + "[2606:4700:4700::1111]:53", + "[2001:4860:4860::8888]:53", + "[2606:4700:4700::1111]:443", + "[2001:4860:4860::8888]:443", + ], + }, + up_command: "", + down_command: "", +}; + const jsonResponse = (body: unknown, init: ResponseInit = { status: 200 }): FetchResponse => new Response(JSON.stringify(body), { ...init, @@ -368,4 +488,54 @@ describe("gluetun schemas", () => { test("vpn settings schema rejects a non-string type", () => { expect(() => gluetunVpnSettingsSchema.parse({ ...VPN_SETTINGS_PAYLOAD, type: 1 })).toThrow(); }); + + test("vpn settings schema parses the live gluetun-latest custom/OpenVPN payload", () => { + const parsed = gluetunVpnSettingsSchema.parse(ALT_VPN_SETTINGS_PAYLOAD); + expect(parsed.type).toBe("openvpn"); + expect(parsed.provider.name).toBe("custom"); + }); + + test("vpn settings schema accepts null server-selection hostnames", () => { + const payload = { + ...VPN_SETTINGS_PAYLOAD, + provider: { + ...VPN_SETTINGS_PAYLOAD.provider, + server_selection: { + ...VPN_SETTINGS_PAYLOAD.provider.server_selection, + hostnames: null, + }, + }, + }; + + expect(() => gluetunVpnSettingsSchema.parse(payload)).not.toThrow(); + }); + + test("vpn settings schema accepts listening_port as a number or a list", () => { + const asList = { + ...VPN_SETTINGS_PAYLOAD, + provider: { + ...VPN_SETTINGS_PAYLOAD.provider, + port_forwarding: { + ...VPN_SETTINGS_PAYLOAD.provider.port_forwarding, + listening_port: [0], + }, + }, + }; + + // The happy-path fixture already exercises the scalar form (listening_port: 0). + expect(() => gluetunVpnSettingsSchema.parse(VPN_SETTINGS_PAYLOAD)).not.toThrow(); + expect(() => gluetunVpnSettingsSchema.parse(asList)).not.toThrow(); + }); + + test("vpn settings schema accepts null wireguard addresses", () => { + const payload = { + ...VPN_SETTINGS_PAYLOAD, + wireguard: { + ...VPN_SETTINGS_PAYLOAD.wireguard, + addresses: null, + }, + }; + + expect(() => gluetunVpnSettingsSchema.parse(payload)).not.toThrow(); + }); }); diff --git a/packages/integrations/src/gotify/gotify-integration.ts b/packages/integrations/src/gotify/gotify-integration.ts index 1d0f5eb8c..829e7abcd 100644 --- a/packages/integrations/src/gotify/gotify-integration.ts +++ b/packages/integrations/src/gotify/gotify-integration.ts @@ -1,12 +1,13 @@ import { ResponseError } from "@homarr/common/server"; import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; +import { ImageProxy } from "@homarr/image-proxy"; import { Integration } from "../base/integration"; import type { IntegrationTestingInput } from "../base/integration"; import type { TestingResult } from "../base/test-connection/test-connection-service"; import type { Notification } from "../interfaces/notifications/notification-types"; import type { INotificationsIntegration } from "../interfaces/notifications/notifications-integration"; -import { gotifyMessagesResponseSchema } from "./gotify-schema"; +import { gotifyApplicationsResponseSchema, gotifyMessagesResponseSchema } from "./gotify-schema"; export class GotifyIntegration extends Integration implements INotificationsIntegration { public async testingAsync(input: IntegrationTestingInput): Promise { @@ -21,23 +22,90 @@ export class GotifyIntegration extends Integration implements INotificationsInte return { Authorization: `Basic ${credentials}` }; } + private getMessagesUrl(applicationId: number) { + const url = this.externalUrl("/"); + url.hash = `/messages/${applicationId}`; + return url.toString(); + } + + private async getApplicationsByIdAsync(): Promise> { + try { + const response = await fetchWithTrustedCertificatesAsync(this.url("/application"), { + headers: this.getHeaders(), + }); + if (!response.ok) throw new ResponseError(response); + + const result = await gotifyApplicationsResponseSchema.safeParseAsync(await response.json()); + if (!result.success) return new Map(); + + return new Map(result.data.map((application) => [application.id, application])); + } catch { + return new Map(); + } + } + + private async getApplicationIconUrlAsync( + imageProxy: ImageProxy, + imagePath: string | null | undefined, + ): Promise { + const trimmedImagePath = imagePath?.trim(); + if (!trimmedImagePath) return undefined; + + try { + const imageUrl = this.getApplicationImageUrl(trimmedImagePath); + return await imageProxy.createImageAsync(imageUrl, this.getHeaders()); + } catch { + return undefined; + } + } + + private getApplicationImageUrl(imagePath: string) { + try { + return new URL(imagePath).toString(); + } catch { + const path = imagePath.startsWith("/") ? imagePath : `/${imagePath}`; + return this.url(path as `/${string}`).toString(); + } + } + public async getNotificationsAsync(): Promise { - const url = this.url("/message", { limit: 100 }); - const response = await fetchWithTrustedCertificatesAsync(url, { headers: this.getHeaders() }); + const [messagesResponse, applicationsById] = await Promise.all([ + fetchWithTrustedCertificatesAsync(this.url("/message", { limit: 100 }), { headers: this.getHeaders() }), + this.getApplicationsByIdAsync(), + ]); - if (!response.ok) throw new ResponseError(response); + if (!messagesResponse.ok) throw new ResponseError(messagesResponse); - const result = await gotifyMessagesResponseSchema.safeParseAsync(await response.json()); - if (!result.success) throw new Error(`Failed to parse Gotify response: ${result.error.message}`); - const parsed = result.data; + const messagesResult = await gotifyMessagesResponseSchema.safeParseAsync(await messagesResponse.json()); + if (!messagesResult.success) throw new Error(`Failed to parse Gotify response: ${messagesResult.error.message}`); - return parsed.messages.map( - (message): Notification => ({ + const imageProxy = new ImageProxy(); + const iconUrlsByApplicationId = new Map(); + await Promise.all( + [...applicationsById.entries()].map(async ([applicationId, application]) => { + iconUrlsByApplicationId.set( + applicationId, + await this.getApplicationIconUrlAsync(imageProxy, application.image), + ); + }), + ); + + return messagesResult.data.messages.map((message): Notification => { + const application = applicationsById.get(message.appid); + + return { id: String(message.id), time: new Date(message.date), title: message.title, body: message.message, - }), - ); + href: this.getMessagesUrl(message.appid), + source: application + ? { + name: application.name, + iconUrl: iconUrlsByApplicationId.get(message.appid), + } + : undefined, + }; + }); } } diff --git a/packages/integrations/src/gotify/gotify-schema.ts b/packages/integrations/src/gotify/gotify-schema.ts index 5c89774ec..ca92535b0 100644 --- a/packages/integrations/src/gotify/gotify-schema.ts +++ b/packages/integrations/src/gotify/gotify-schema.ts @@ -3,6 +3,7 @@ import { z } from "zod/v4"; // See: https://gotify.net/api-docs#/message/getMessages export const gotifyMessageSchema = z.object({ id: z.number(), + appid: z.number(), date: z.string(), title: z.string(), message: z.string(), @@ -12,3 +13,11 @@ export const gotifyMessageSchema = z.object({ export const gotifyMessagesResponseSchema = z.object({ messages: z.array(gotifyMessageSchema), }); + +export const gotifyApplicationSchema = z.object({ + id: z.number(), + name: z.string(), + image: z.string().nullable().optional(), +}); + +export const gotifyApplicationsResponseSchema = z.array(gotifyApplicationSchema); diff --git a/packages/integrations/src/immich/immich-integration.ts b/packages/integrations/src/immich/immich-integration.ts index 9e13ff953..c01386256 100644 --- a/packages/integrations/src/immich/immich-integration.ts +++ b/packages/integrations/src/immich/immich-integration.ts @@ -1,4 +1,13 @@ -import { getAlbumInfo, getAllAlbums, getMyUser, getServerStatistics, init, searchUsers } from "@immich/sdk"; +import { + getAlbumInfo, + getAllAlbums, + getMyUser, + getServerStatistics, + init, + searchAssets, + searchUsers, +} from "@immich/sdk"; +import type { AssetResponseDto, MetadataSearchDto } from "@immich/sdk"; import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { createLogger } from "@homarr/core/infrastructure/logs"; @@ -23,7 +32,6 @@ export interface ImmichAlbum { export interface ImmichAsset { id: string; - deviceAssetId: string; originalPath: string; thumbhash: string | null; fileModifiedAt: string; @@ -56,12 +64,16 @@ export class ImmichIntegration extends Integration { public async getAlbumAsync(albumId: string): Promise { this.initClient(); - const album = await getAlbumInfo({ id: albumId }, this.getRequestOptions()); + const requestOptions = this.getRequestOptions(); + const [album, albumAssets] = await Promise.all([ + getAlbumInfo({ id: albumId }, requestOptions), + this.searchAlbumAssetsAsync(albumId), + ]); const imageProxy = new ImageProxy(); return { albumName: album.albumName, assets: await Promise.all( - album.assets.map(async (asset) => { + albumAssets.map(async (asset) => { const publicLink = await imageProxy.createImageAsync( this.url(`/api/assets/${asset.id}/original`).toString(), { @@ -71,7 +83,6 @@ export class ImmichIntegration extends Integration { return { id: asset.id, type: asset.type, - deviceAssetId: asset.deviceAssetId, thumbhash: asset.thumbhash, fileCreatedAt: asset.fileCreatedAt, fileModifiedAt: asset.fileModifiedAt, @@ -104,6 +115,24 @@ export class ImmichIntegration extends Integration { return { success: true }; } + private async searchAlbumAssetsAsync(albumId: string): Promise { + const requestOptions = this.getRequestOptions(); + const assets: AssetResponseDto[] = []; + let nextPage: string | null = null; + + do { + const metadataSearchDto: MetadataSearchDto = { albumIds: [albumId] }; + if (nextPage !== null) { + metadataSearchDto.page = Number(nextPage); + } + const searchResult = await searchAssets({ metadataSearchDto }, requestOptions); + assets.push(...searchResult.assets.items); + nextPage = searchResult.assets.nextPage; + } while (nextPage !== null); + + return assets; + } + private getRequestOptions(fetchAsync = fetchWithTrustedCertificatesAsync) { return { // Undici and node types are not the same diff --git a/packages/integrations/src/index.ts b/packages/integrations/src/index.ts index d78c24bb5..df128971b 100644 --- a/packages/integrations/src/index.ts +++ b/packages/integrations/src/index.ts @@ -1,6 +1,6 @@ // General integrations -export { AdGuardHomeIntegration } from "./adguard-home/adguard-home-integration"; export { ArchiveTeamWarriorIntegration } from "./archive-team-warrior/archive-team-warrior-integration"; +export { AdGuardHomeIntegration } from "./adguard-home/adguard-home-integration"; export { TechnitiumDnsIntegration } from "./technitium/technitium-integration"; export { AnchorIntegration } from "./anchor/anchor-integration"; export { Aria2Integration } from "./download-client/aria2/aria2-integration"; @@ -29,6 +29,7 @@ export { PiHoleIntegrationV6 } from "./pi-hole/v6/pi-hole-integration-v6"; export { PlexIntegration } from "./plex/plex-integration"; export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration"; export { TrueNasIntegration } from "./truenas/truenas-integration"; +export { SynologyIntegration } from "./synology/synology-integration"; export { UnraidIntegration } from "./unraid/unraid-integration"; export { OPNsenseIntegration } from "./opnsense/opnsense-integration"; export { ICalIntegration } from "./ical/ical-integration"; @@ -42,6 +43,8 @@ export { UptimeKumaIntegration } from "./uptime-kuma/uptime-kuma-integration"; export { UmamiIntegration } from "./umami/umami-integration"; export { PeaNutIntegration } from "./peanut/peanut-integration"; export { OpenWebUiIntegration } from "./open-webui/open-webui-integration"; +export { BazarrIntegration } from "./bazarr/bazarr-integration"; +export { TraefikIntegration } from "./traefik/traefik-integration"; // Types export type { IntegrationInput } from "./base/integration"; @@ -65,10 +68,10 @@ export type { TdarrStatistics, TdarrWorker, } from "./interfaces/media-transcoding/media-transcoding-types"; -export type { ReleasesRepository, ReleaseResponse } from "./interfaces/releases-providers/releases-providers-types"; export type { Notification } from "./interfaces/notifications/notification-types"; export type { ImmichServerStats, ImmichAlbum, ImmichAsset } from "./immich/immich-integration"; export type { PaperlessNgxStats } from "./paperless-ngx/paperless-ngx-types"; +export type { PatchMonStats, PatchMonOsDistributionEntry } from "./patchmon/patchmon-types"; export type { AnchorNote, AnchorNotePermission, @@ -76,17 +79,19 @@ export type { AnchorNotesListInput, AnchorNoteSummary, } from "./anchor/anchor-types"; -export type { - ArchiveTeamWarriorBandwidth, - ArchiveTeamWarriorItem, - ArchiveTeamWarriorStatus, -} from "./archive-team-warrior/archive-team-warrior-types"; export type { TracearrDashboardData } from "./tracearr/tracearr-types"; export type { SpeedtestTrackerDashboardData } from "./speedtest-tracker/speedtest-tracker-types"; export type { AudiobookshelfDashboardData } from "./audiobookshelf/audiobookshelf-types"; export type { NavidromeDashboardData } from "./navidrome/navidrome-types"; export type { UptimeKumaDashboardData } from "./uptime-kuma/uptime-kuma-types"; export type { UmamiVisitorStats } from "./umami/umami-types"; +export type { BazarrBadges } from "./bazarr/bazarr-types"; +export type { TraefikDashboardData, TraefikProtocolSummary, TraefikResourceSummary } from "./traefik/traefik-types"; +export type { + ArchiveTeamWarriorBandwidth, + ArchiveTeamWarriorItem, + ArchiveTeamWarriorStatus, +} from "./archive-team-warrior/archive-team-warrior-types"; // Schemas export { anchorNotesListInputSchema } from "./anchor/anchor-types"; diff --git a/packages/integrations/src/interfaces/media-organizer/media-organizer-integration.ts b/packages/integrations/src/interfaces/media-organizer/media-organizer-integration.ts new file mode 100644 index 000000000..daeae93b4 --- /dev/null +++ b/packages/integrations/src/interfaces/media-organizer/media-organizer-integration.ts @@ -0,0 +1,6 @@ +import type { MissingMediaItem, QueuedMediaItem } from "./media-organizer-types"; + +export interface IMediaOrganizerIntegration { + getMissingAsync(pageSize?: number): Promise<{ items: MissingMediaItem[]; totalCount: number }>; + getMediaQueueAsync(pageSize?: number): Promise<{ items: QueuedMediaItem[]; totalCount: number }>; +} diff --git a/packages/integrations/src/interfaces/media-organizer/media-organizer-types.ts b/packages/integrations/src/interfaces/media-organizer/media-organizer-types.ts new file mode 100644 index 000000000..fff59e38a --- /dev/null +++ b/packages/integrations/src/interfaces/media-organizer/media-organizer-types.ts @@ -0,0 +1,26 @@ +export interface MissingMediaItem { + id: number; + title: string; + type: "movie" | "episode"; + year?: number; + seasonNumber?: number; + episodeNumber?: number; + seriesTitle?: string; + imageUrl?: string | null; + link: string; +} + +export interface QueuedMediaItem { + id: number; + title: string; + type: "movie" | "episode"; + status: string; + timeLeft: string | null; + percentComplete: number; + year?: number; + seasonNumber?: number; + episodeNumber?: number; + seriesTitle?: string; + imageUrl?: string | null; + link: string; +} diff --git a/packages/integrations/src/interfaces/notifications/notification-types.ts b/packages/integrations/src/interfaces/notifications/notification-types.ts index 8f6944944..252633e9e 100644 --- a/packages/integrations/src/interfaces/notifications/notification-types.ts +++ b/packages/integrations/src/interfaces/notifications/notification-types.ts @@ -3,4 +3,9 @@ export interface Notification { time: Date; title: string; body: string; + href?: string; + source?: { + name: string; + iconUrl?: string; + }; } diff --git a/packages/integrations/src/interfaces/releases-providers/releases-providers-integration.ts b/packages/integrations/src/interfaces/releases-providers/releases-providers-integration.ts deleted file mode 100644 index e94f555fc..000000000 --- a/packages/integrations/src/interfaces/releases-providers/releases-providers-integration.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { ReleaseProviderResponse, ReleaseResponse } from "./releases-providers-types"; - -export const getLatestRelease = ( - releases: ReleaseProviderResponse[], - versionRegex?: string, -): ReleaseProviderResponse | null => { - const validReleases = releases.filter((result) => { - if (result.latestRelease) { - return versionRegex ? new RegExp(versionRegex).test(result.latestRelease) : true; - } - return true; - }); - - return validReleases.length === 0 - ? null - : validReleases.reduce((latest, current) => (current.latestReleaseAt > latest.latestReleaseAt ? current : latest)); -}; - -export interface ReleasesProviderIntegration { - getLatestMatchingReleaseAsync(identifier: string, versionRegex?: string): Promise; -} diff --git a/packages/integrations/src/interfaces/releases-providers/releases-providers-types.ts b/packages/integrations/src/interfaces/releases-providers/releases-providers-types.ts deleted file mode 100644 index fb89b0fcb..000000000 --- a/packages/integrations/src/interfaces/releases-providers/releases-providers-types.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { TranslationObject } from "@homarr/translation"; - -export interface ReleasesRepository extends Record { - id: string; - identifier: string; - versionRegex?: string; -} - -export interface DetailsProviderResponse { - projectUrl?: string; - projectDescription?: string; - isFork?: boolean; - isArchived?: boolean; - createdAt?: Date; - starsCount?: number; - openIssues?: number; - forksCount?: number; -} - -export interface ReleaseProviderResponse { - latestRelease: string; - latestReleaseAt: Date; - releaseUrl?: string; - releaseDescription?: string; - isPreRelease?: boolean; -} - -type ReleasesErrorKeys = keyof TranslationObject["widget"]["releases"]["error"]["messages"]; - -export type ReleaseData = DetailsProviderResponse & ReleaseProviderResponse; - -export type ReleaseError = { code: ReleasesErrorKeys } | { code: "unexpected"; message: string }; - -export type ReleaseResponse = { success: true; data: ReleaseData } | { success: false; error: ReleaseError }; diff --git a/packages/integrations/src/interfaces/timetable/timetable-integration.ts b/packages/integrations/src/interfaces/timetable/timetable-integration.ts deleted file mode 100644 index 74e9d3c02..000000000 --- a/packages/integrations/src/interfaces/timetable/timetable-integration.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { Station, Timetable, TimetableOptions } from "./timetable-types"; - -export interface TimetableIntegration { - searchStationsAsync(query: string): Promise; - getTimetableAsync(options: TimetableOptions): Promise; -} diff --git a/packages/integrations/src/interfaces/timetable/timetable-types.ts b/packages/integrations/src/interfaces/timetable/timetable-types.ts deleted file mode 100644 index 72350b5a3..000000000 --- a/packages/integrations/src/interfaces/timetable/timetable-types.ts +++ /dev/null @@ -1,33 +0,0 @@ -import z from "zod"; - -export interface Station { - id: string; - name: string; -} - -export const timetableOptionsSchema = z.object({ - stationId: z.string(), - limit: z.number().default(10), -}); - -export type TimetableOptions = z.infer; - -export interface TimetableEntry { - timestamp: Date; - delay: number; - line: { - name: string; - color: string | null; - } | null; - location: string; - platform: { - name: string; - hasChanged: boolean; - } | null; -} - -export interface Timetable { - stationId: string; - timestamp: Date; - entries: TimetableEntry[]; -} diff --git a/packages/integrations/src/linuxserverio/linuxserverio-integration.ts b/packages/integrations/src/linuxserverio/linuxserverio-integration.ts deleted file mode 100644 index 3178f2c4b..000000000 --- a/packages/integrations/src/linuxserverio/linuxserverio-integration.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; -import { createLogger } from "@homarr/core/infrastructure/logs"; - -import type { IntegrationTestingInput } from "../base/integration"; -import { Integration } from "../base/integration"; -import { TestConnectionError } from "../base/test-connection/test-connection-error"; -import type { TestingResult } from "../base/test-connection/test-connection-service"; -import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration"; -import type { ReleaseResponse } from "../interfaces/releases-providers/releases-providers-types"; -import { releasesResponseSchema } from "./linuxserverio-schemas"; - -const logger = createLogger({ module: "linuxServerIOIntegration" }); - -export class LinuxServerIOIntegration extends Integration implements ReleasesProviderIntegration { - protected async testingAsync(input: IntegrationTestingInput): Promise { - const response = await input.fetchAsync(this.url("/health")); - - if (!response.ok) { - return TestConnectionError.StatusResult(response); - } - - return { - success: true, - }; - } - - private parseIdentifier(identifier: string) { - const [owner, name] = identifier.split("/"); - if (!owner || !name) { - logger.warn("Invalid identifier format. Expected 'owner/name' for identifier", { identifier }); - return null; - } - return { owner, name }; - } - - public async getLatestMatchingReleaseAsync(identifier: string): Promise { - const { name } = this.parseIdentifier(identifier) ?? {}; - if (!name) return { success: false, error: { code: "invalidIdentifier" } }; - - const releasesResponse = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/images")); - if (!releasesResponse.ok) { - return { success: false, error: { code: "unexpected", message: releasesResponse.statusText } }; - } - - const releasesResponseJson: unknown = await releasesResponse.json(); - const { data, success, error } = releasesResponseSchema.safeParse(releasesResponseJson); - if (!success) { - return { success: false, error: { code: "unexpected", message: error.message } }; - } - - const release = data.data.repositories.linuxserver.find((repo) => repo.name === name); - if (!release) { - logger.warn("Repository not found on provider", { - name, - }); - return { success: false, error: { code: "noMatchingVersion" } }; - } - - return { - success: true, - data: { - latestRelease: release.version, - latestReleaseAt: release.version_timestamp, - releaseDescription: release.changelog?.shift()?.desc, - projectUrl: release.github_url, - projectDescription: release.description, - isArchived: release.deprecated, - createdAt: release.initial_date ? new Date(release.initial_date) : undefined, - starsCount: release.stars, - }, - }; - } -} diff --git a/packages/integrations/src/linuxserverio/linuxserverio-schemas.ts b/packages/integrations/src/linuxserverio/linuxserverio-schemas.ts deleted file mode 100644 index 3e3653697..000000000 --- a/packages/integrations/src/linuxserverio/linuxserverio-schemas.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { z } from "zod/v4"; - -export const releasesResponseSchema = z.object({ - data: z.object({ - repositories: z.object({ - linuxserver: z.array( - z.object({ - name: z.string(), - initial_date: z - .string() - .transform((value) => new Date(value)) - .optional(), - github_url: z.string(), - description: z.string(), - version: z.string(), - version_timestamp: z.string().transform((value) => new Date(value)), - stars: z.number(), - deprecated: z.boolean(), - changelog: z - .array( - z.object({ - date: z.string().transform((value) => new Date(value)), - desc: z.string(), - }), - ) - .optional(), - }), - ), - }), - }), -}); diff --git a/packages/integrations/src/media-organizer/lidarr/lidarr-integration.ts b/packages/integrations/src/media-organizer/lidarr/lidarr-integration.ts index 0bac3ec30..5b16b3ea8 100644 --- a/packages/integrations/src/media-organizer/lidarr/lidarr-integration.ts +++ b/packages/integrations/src/media-organizer/lidarr/lidarr-integration.ts @@ -69,36 +69,24 @@ export class LidarrIntegration extends Integration implements ICalendarIntegrati private getLinksForLidarrCalendarEvent = (event: z.infer) => { const links: CalendarLink[] = []; + // titleSlug is only present when Lidarr returns it; guard so a missing value + // neither breaks the link nor drops the event. + if (event.artist.titleSlug) { + links.push({ + href: this.externalUrl(`/artist/${event.artist.titleSlug}`).toString(), + name: "Lidarr", + logo: "/images/apps/lidarr.svg", + color: undefined, + isDark: true, + }); + } + for (const link of event.artist.links) { - switch (link.name) { - case "vgmdb": - links.push({ - href: link.url, - name: "VgmDB", - color: "#f5c518", - isDark: false, - logo: "/images/apps/vgmdb.svg", - }); - break; - case "imdb": - links.push({ - href: link.url, - name: "IMDb", - color: "#f5c518", - isDark: false, - logo: "/images/apps/imdb.png", - }); - break; - case "last": - links.push({ - href: link.url, - name: "LastFM", - color: "#cf222a", - isDark: false, - logo: "/images/apps/lastfm.svg", - }); - break; + const externalLink = lidarrExternalCalendarLinks[link.name]; + if (!externalLink) { + continue; } + links.push({ ...externalLink, href: link.url }); } return links; @@ -109,7 +97,7 @@ export class LidarrIntegration extends Integration implements ICalendarIntegrati ): z.infer["images"][number] | undefined => { const flatImages = [...event.images]; - const sortedImages = flatImages.sort( + const sortedImages = flatImages.toSorted( (imageA, imageB) => mediaOrganizerPriorities.indexOf(imageA.coverType) - mediaOrganizerPriorities.indexOf(imageB.coverType), ); @@ -149,6 +137,16 @@ const lidarrCalendarEventSchema = z.object({ title: z.string(), overview: z.string().optional(), images: lidarrCalendarEventImageSchema, - artist: z.object({ links: z.array(z.object({ url: z.string().url(), name: z.string() })), artistName: z.string() }), + artist: z.object({ + links: z.array(z.object({ url: z.string().url(), name: z.string() })), + artistName: z.string(), + titleSlug: z.string().optional(), + }), releaseDate: z.string().transform((value) => new Date(value)), }); + +const lidarrExternalCalendarLinks: Record> = { + vgmdb: { name: "VgmDB", color: "#f5c518", isDark: false, logo: "/images/apps/vgmdb.svg" }, + imdb: { name: "IMDb", color: "#f5c518", isDark: false, logo: "/images/apps/imdb.png" }, + last: { name: "LastFM", color: "#cf222a", isDark: false, logo: "/images/apps/lastfm.svg" }, +}; diff --git a/packages/integrations/src/media-organizer/radarr/radarr-integration.ts b/packages/integrations/src/media-organizer/radarr/radarr-integration.ts index b557f7b77..aa116d1e5 100644 --- a/packages/integrations/src/media-organizer/radarr/radarr-integration.ts +++ b/packages/integrations/src/media-organizer/radarr/radarr-integration.ts @@ -10,11 +10,13 @@ import type { TestingResult } from "../../base/test-connection/test-connection-s import type { ICalendarIntegration } from "../../interfaces/calendar/calendar-integration"; import type { CalendarEvent, CalendarLink } from "../../interfaces/calendar/calendar-types"; import { radarrReleaseTypes } from "../../interfaces/calendar/calendar-types"; +import type { IMediaOrganizerIntegration } from "../../interfaces/media-organizer/media-organizer-integration"; +import type { MissingMediaItem, QueuedMediaItem } from "../../interfaces/media-organizer/media-organizer-types"; import { mediaOrganizerPriorities } from "../media-organizer"; const logger = createLogger({ module: "radarrIntegration" }); -export class RadarrIntegration extends Integration implements ICalendarIntegration { +export class RadarrIntegration extends Integration implements ICalendarIntegration, IMediaOrganizerIntegration { /** * Gets the events in the Radarr calendar between two dates. * @param start The start date @@ -111,6 +113,79 @@ export class RadarrIntegration extends Integration implements ICalendarIntegrati return bestImage.remoteUrl; }; + async getMissingAsync(pageSize = 10): Promise<{ items: MissingMediaItem[]; totalCount: number }> { + const url = this.url("/api/v3/wanted/missing", { + page: 1, + pageSize, + sortKey: "title", + sortDirection: "ascending", + }); + + const response = await fetchWithTrustedCertificatesAsync(url, { + headers: { "X-Api-Key": super.getSecretValue("apiKey") }, + }); + const data = await z + .object({ + totalRecords: z.number(), + records: z.array(radarrMissingMovieSchema), + }) + .parseAsync(await response.json()); + + return { + totalCount: data.totalRecords, + items: data.records.map( + (movie): MissingMediaItem => ({ + id: movie.id, + title: movie.title, + type: "movie", + year: movie.year, + imageUrl: movie.images.find((img) => img.coverType === "poster")?.remoteUrl ?? null, + link: this.externalUrl(`/movie/${movie.titleSlug}`).toString(), + }), + ), + }; + } + + async getMediaQueueAsync(pageSize = 10): Promise<{ items: QueuedMediaItem[]; totalCount: number }> { + const url = this.url("/api/v3/queue", { + page: 1, + pageSize, + includeMovie: true, + }); + + const response = await fetchWithTrustedCertificatesAsync(url, { + headers: { "X-Api-Key": super.getSecretValue("apiKey") }, + }); + const data = await z + .object({ + totalRecords: z.number(), + records: z.array(radarrQueueItemSchema), + }) + .parseAsync(await response.json()); + + return { + totalCount: data.totalRecords, + items: data.records.map((item): QueuedMediaItem => { + const sizeLeft = item.sizeleft ?? 0; + const sizeTotal = item.size ?? 0; + const percentComplete = sizeTotal > 0 ? Math.round(((sizeTotal - sizeLeft) / sizeTotal) * 100) : 0; + return { + id: item.id, + title: item.movie?.title ?? item.title, + type: "movie", + status: item.status, + timeLeft: item.timeleft ?? null, + percentComplete, + year: item.movie?.year, + imageUrl: item.movie?.images.find((img) => img.coverType === "poster")?.remoteUrl ?? null, + link: item.movie?.titleSlug + ? this.externalUrl(`/movie/${item.movie.titleSlug}`).toString() + : this.externalUrl("/activity/queue").toString(), + }; + }), + }; + } + protected async testingAsync(input: IntegrationTestingInput): Promise { const response = await input.fetchAsync(this.url("/api"), { headers: { "X-Api-Key": super.getSecretValue("apiKey") }, @@ -131,6 +206,41 @@ const radarrCalendarEventImageSchema = z.array( }), ); +const radarrMissingMovieSchema = z.object({ + id: z.number(), + title: z.string(), + year: z.number().optional(), + titleSlug: z.string(), + images: z.array( + z.object({ + coverType: z.string(), + remoteUrl: z.string().url(), + }), + ), +}); + +const radarrQueueItemSchema = z.object({ + id: z.number(), + title: z.string(), + status: z.string(), + timeleft: z.string().optional(), + size: z.number().optional(), + sizeleft: z.number().optional(), + movie: z + .object({ + title: z.string(), + year: z.number().optional(), + titleSlug: z.string(), + images: z.array( + z.object({ + coverType: z.string(), + remoteUrl: z.string().url(), + }), + ), + }) + .optional(), +}); + const radarrCalendarEventSchema = z.object({ title: z.string(), originalTitle: z.string(), diff --git a/packages/integrations/src/media-organizer/sonarr/sonarr-integration.ts b/packages/integrations/src/media-organizer/sonarr/sonarr-integration.ts index 17826e2de..43810ae19 100644 --- a/packages/integrations/src/media-organizer/sonarr/sonarr-integration.ts +++ b/packages/integrations/src/media-organizer/sonarr/sonarr-integration.ts @@ -9,11 +9,13 @@ import { TestConnectionError } from "../../base/test-connection/test-connection- import type { TestingResult } from "../../base/test-connection/test-connection-service"; import type { ICalendarIntegration } from "../../interfaces/calendar/calendar-integration"; import type { CalendarEvent, CalendarLink } from "../../interfaces/calendar/calendar-types"; +import type { IMediaOrganizerIntegration } from "../../interfaces/media-organizer/media-organizer-integration"; +import type { MissingMediaItem, QueuedMediaItem } from "../../interfaces/media-organizer/media-organizer-types"; import { mediaOrganizerPriorities } from "../media-organizer"; const logger = createLogger({ module: "sonarrIntegration" }); -export class SonarrIntegration extends Integration implements ICalendarIntegration { +export class SonarrIntegration extends Integration implements ICalendarIntegration, IMediaOrganizerIntegration { /** * Gets the events in the Sonarr calendar between two dates. * @param start The start date @@ -107,6 +109,88 @@ export class SonarrIntegration extends Integration implements ICalendarIntegrati return bestImage.remoteUrl; }; + async getMissingAsync(pageSize = 10): Promise<{ items: MissingMediaItem[]; totalCount: number }> { + const url = this.url("/api/v3/wanted/missing", { + page: 1, + pageSize, + sortKey: "airDateUtc", + sortDirection: "ascending", + includeSeries: true, + }); + + const response = await fetchWithTrustedCertificatesAsync(url, { + headers: { "X-Api-Key": super.getSecretValue("apiKey") }, + }); + const data = await z + .object({ + totalRecords: z.number(), + records: z.array(sonarrMissingEpisodeSchema), + }) + .parseAsync(await response.json()); + + return { + totalCount: data.totalRecords, + items: data.records.map( + (episode): MissingMediaItem => ({ + id: episode.id, + title: episode.title, + type: "episode", + seasonNumber: episode.seasonNumber, + episodeNumber: episode.episodeNumber, + seriesTitle: episode.series?.title, + year: episode.series?.year, + imageUrl: episode.series?.images.find((img) => img.coverType === "poster")?.remoteUrl ?? null, + link: episode.series?.titleSlug + ? this.externalUrl(`/series/${episode.series.titleSlug}`).toString() + : this.externalUrl("/wanted/missing").toString(), + }), + ), + }; + } + + async getMediaQueueAsync(pageSize = 10): Promise<{ items: QueuedMediaItem[]; totalCount: number }> { + const url = this.url("/api/v3/queue", { + page: 1, + pageSize, + includeSeries: true, + includeEpisode: true, + }); + + const response = await fetchWithTrustedCertificatesAsync(url, { + headers: { "X-Api-Key": super.getSecretValue("apiKey") }, + }); + const data = await z + .object({ + totalRecords: z.number(), + records: z.array(sonarrQueueItemSchema), + }) + .parseAsync(await response.json()); + + return { + totalCount: data.totalRecords, + items: data.records.map((item): QueuedMediaItem => { + const sizeLeft = item.sizeleft ?? 0; + const sizeTotal = item.size ?? 0; + const percentComplete = sizeTotal > 0 ? Math.round(((sizeTotal - sizeLeft) / sizeTotal) * 100) : 0; + return { + id: item.id, + title: item.episode?.title ?? item.title, + type: "episode", + status: item.status, + timeLeft: item.timeleft ?? null, + percentComplete, + seasonNumber: item.episode?.seasonNumber, + episodeNumber: item.episode?.episodeNumber, + seriesTitle: item.series?.title, + imageUrl: item.series?.images.find((img) => img.coverType === "poster")?.remoteUrl ?? null, + link: item.series?.titleSlug + ? this.externalUrl(`/series/${item.series.titleSlug}`).toString() + : this.externalUrl("/activity/queue").toString(), + }; + }), + }; + } + protected async testingAsync(input: IntegrationTestingInput): Promise { const response = await input.fetchAsync(this.url("/api"), { headers: { "X-Api-Key": super.getSecretValue("apiKey") }, @@ -119,6 +203,54 @@ export class SonarrIntegration extends Integration implements ICalendarIntegrati } } +const sonarrMissingEpisodeSchema = z.object({ + id: z.number(), + title: z.string(), + seasonNumber: z.number(), + episodeNumber: z.number(), + series: z + .object({ + title: z.string(), + year: z.number().optional(), + titleSlug: z.string(), + images: z.array( + z.object({ + coverType: z.string(), + remoteUrl: z.string().url(), + }), + ), + }) + .optional(), +}); + +const sonarrQueueItemSchema = z.object({ + id: z.number(), + title: z.string(), + status: z.string(), + timeleft: z.string().optional(), + size: z.number().optional(), + sizeleft: z.number().optional(), + series: z + .object({ + title: z.string(), + titleSlug: z.string(), + images: z.array( + z.object({ + coverType: z.string(), + remoteUrl: z.string().url(), + }), + ), + }) + .optional(), + episode: z + .object({ + title: z.string(), + seasonNumber: z.number(), + episodeNumber: z.number(), + }) + .optional(), +}); + const sonarrCalendarEventImageSchema = z.array( z.object({ // See https://github.com/Sonarr/Sonarr/blob/9e5ebdc6245d4714776b53127a1e6b63c25fbcb9/src/NzbDrone.Core/MediaCover/MediaCover.cs#L5-L14 diff --git a/packages/integrations/src/mock/data/beszel.ts b/packages/integrations/src/mock/data/beszel.ts index c6998aa84..f79de9d99 100644 --- a/packages/integrations/src/mock/data/beszel.ts +++ b/packages/integrations/src/mock/data/beszel.ts @@ -7,6 +7,7 @@ import type { BeszelSystemDetails, BeszelSystemStats, BeszelSystemStatsRecord, + LiveStatsEvent, } from "../../beszel/beszel-types"; const GB = 1024 * 1024 * 1024; @@ -225,6 +226,7 @@ export class BeszelMockService { u: randInt(86400, 2_000_000), mp: clamp(sys.baseMem + rand(-5, 5), 0, 100), dp: clamp(sys.baseDisk + rand(-2, 2), 0, 100), + efs: i === 0 ? { "/mnt/sda": 86.9, "/mnt/sdb": 70.5, "/mnt/sdc": 42.7 } : undefined, bb: rand(200, 3000) * KB, v: "0.8.2", g: i === 3 ? rand(15, 45) : 0, @@ -332,7 +334,7 @@ export class BeszelMockService { public async subscribeRealtimeMetrics( systemId: string, - onMessage: (data: { stats: BeszelSystemStatsRecord; containerStats: BeszelContainerStatsRecord | null }) => void, + onMessage: (event: LiveStatsEvent) => void, signal: AbortSignal, ): Promise { const sys = resolveSystem(systemId); @@ -348,7 +350,7 @@ export class BeszelMockService { created: new Date().toISOString(), updated: new Date().toISOString(), }; - onMessage({ stats: record, containerStats: null }); + onMessage({ type: "system_stats", record }); }; if (signal.aborted) return; await new Promise((resolve) => { diff --git a/packages/integrations/src/mock/data/media-organizer.ts b/packages/integrations/src/mock/data/media-organizer.ts new file mode 100644 index 000000000..1c12d1621 --- /dev/null +++ b/packages/integrations/src/mock/data/media-organizer.ts @@ -0,0 +1,116 @@ +import type { IMediaOrganizerIntegration } from "../../interfaces/media-organizer/media-organizer-integration"; +import type { MissingMediaItem, QueuedMediaItem } from "../../interfaces/media-organizer/media-organizer-types"; + +const missingItems: MissingMediaItem[] = [ + { + id: 1, + title: "Dune: Part Two", + type: "movie", + year: 2024, + imageUrl: "https://image.tmdb.org/t/p/w300/1pdfLvkbY9ohJlCjQH2CZjjYVvJ.jpg", + link: "https://www.imdb.com/title/tt15239678/", + }, + { + id: 2, + title: "The Batman Part II", + type: "movie", + year: 2026, + imageUrl: "https://image.tmdb.org/t/p/w300/74xTEgt7R36Fpooo50r9T25onhq.jpg", + link: "https://www.imdb.com/title/tt7126948/", + }, + { + id: 3, + title: "The Last of Us", + type: "episode", + seasonNumber: 2, + episodeNumber: 5, + seriesTitle: "The Last of Us", + year: 2025, + imageUrl: "https://image.tmdb.org/t/p/w300/uKvVjHNqB5VmOrdxqAt2F7J78ED.jpg", + link: "https://www.imdb.com/title/tt3581920/", + }, + { + id: 4, + title: "Andor", + type: "episode", + seasonNumber: 2, + episodeNumber: 3, + seriesTitle: "Andor", + year: 2025, + imageUrl: "https://image.tmdb.org/t/p/w300/59SVNwLfoMnZPPB6ukW6dlPxAdI.jpg", + link: "https://www.imdb.com/title/tt9253284/", + }, + { + id: 5, + title: "Gladiator II", + type: "movie", + year: 2024, + imageUrl: "https://image.tmdb.org/t/p/w300/2cxhvwyEwRlysAmRH4iodkvo0z5.jpg", + link: "https://www.imdb.com/title/tt9218128/", + }, +]; + +const queuedItems: QueuedMediaItem[] = [ + { + id: 101, + title: "Oppenheimer", + type: "movie", + status: "downloading", + timeLeft: "1h 23m", + percentComplete: 62, + imageUrl: "https://image.tmdb.org/t/p/w300/8Gxv8gSFCU0XGDykEGv7zR1n2ua.jpg", + link: "https://www.imdb.com/title/tt15398776/", + }, + { + id: 102, + title: "Shogun", + type: "episode", + seasonNumber: 1, + episodeNumber: 8, + seriesTitle: "Shogun", + status: "downloading", + timeLeft: "47m", + percentComplete: 81, + imageUrl: "https://image.tmdb.org/t/p/w300/7O4iVfOMQmdCSxhOg1WnzG1AgYT.jpg", + link: "https://www.imdb.com/title/tt2098771/", + }, + { + id: 103, + title: "The Substance", + type: "movie", + status: "queued", + timeLeft: null, + percentComplete: 0, + imageUrl: "https://image.tmdb.org/t/p/w300/lqoMzCcZYEFK729d6qzt349fB4o.jpg", + link: "https://www.imdb.com/title/tt17526714/", + }, + { + id: 104, + title: "House of the Dragon", + type: "episode", + seasonNumber: 2, + episodeNumber: 7, + seriesTitle: "House of the Dragon", + status: "importing", + timeLeft: "3m", + percentComplete: 99, + imageUrl: "https://image.tmdb.org/t/p/w300/z2yahl2uefxDCl0nogcRBstwruJ.jpg", + link: "https://www.imdb.com/title/tt11198330/", + }, +]; + +export class MediaOrganizerMockService implements IMediaOrganizerIntegration { + async getMissingAsync(pageSize = 10) { + return { + items: missingItems.slice(0, pageSize), + totalCount: missingItems.length, + }; + } + + async getMediaQueueAsync(pageSize = 10) { + return { + items: queuedItems.slice(0, pageSize), + totalCount: queuedItems.length, + }; + } +} diff --git a/packages/integrations/src/mock/mock-integration.ts b/packages/integrations/src/mock/mock-integration.ts index d7eb412cf..90c3905da 100644 --- a/packages/integrations/src/mock/mock-integration.ts +++ b/packages/integrations/src/mock/mock-integration.ts @@ -9,6 +9,7 @@ import type { ISystemHealthMonitoringIntegration, } from "../interfaces/health-monitoring/health-monitoring-integration"; import type { IIndexerManagerIntegration } from "../interfaces/indexer-manager/indexer-manager-integration"; +import type { IMediaOrganizerIntegration } from "../interfaces/media-organizer/media-organizer-integration"; import type { IMediaReleasesIntegration } from "../interfaces/media-releases"; import type { IMediaRequestIntegration } from "../interfaces/media-requests/media-request-integration"; import type { IMediaServerIntegration } from "../interfaces/media-server/media-server-integration"; @@ -21,6 +22,7 @@ import { ClusterHealthMonitoringMockService } from "./data/cluster-health-monito import { DnsHoleMockService } from "./data/dns-hole"; import { DownloadClientMockService } from "./data/download"; import { IndexerManagerMockService } from "./data/indexer-manager"; +import { MediaOrganizerMockService } from "./data/media-organizer"; import { MediaReleasesMockService } from "./data/media-releases"; import { MediaRequestMockService } from "./data/media-request"; import { MediaServerMockService } from "./data/media-server"; @@ -41,6 +43,7 @@ export class MockIntegration IClusterHealthMonitoringIntegration, ISystemHealthMonitoringIntegration, IIndexerManagerIntegration, + IMediaOrganizerIntegration, IMediaReleasesIntegration, IMediaRequestIntegration, IMediaServerIntegration, @@ -55,6 +58,7 @@ export class MockIntegration private static readonly clusterMonitoring = new ClusterHealthMonitoringMockService(); private static readonly systemMonitoring = new SystemHealthMonitoringMockService(); private static readonly indexerManager = new IndexerManagerMockService(); + private static readonly mediaOrganizer = new MediaOrganizerMockService(); private static readonly mediaReleases = new MediaReleasesMockService(); private static readonly mediaRequest = new MediaRequestMockService(); private static readonly mediaServer = new MediaServerMockService(); @@ -97,6 +101,10 @@ export class MockIntegration getIndexersAsync = MockIntegration.indexerManager.getIndexersAsync.bind(MockIntegration.indexerManager); testAllAsync = MockIntegration.indexerManager.testAllAsync.bind(MockIntegration.indexerManager); + // MediaOrganizerIntegration + getMissingAsync = MockIntegration.mediaOrganizer.getMissingAsync.bind(MockIntegration.mediaOrganizer); + getMediaQueueAsync = MockIntegration.mediaOrganizer.getMediaQueueAsync.bind(MockIntegration.mediaOrganizer); + // MediaReleasesIntegration getMediaReleasesAsync = MockIntegration.mediaReleases.getMediaReleasesAsync.bind(MockIntegration.mediaReleases); diff --git a/packages/integrations/src/npm/npm-integration.ts b/packages/integrations/src/npm/npm-integration.ts deleted file mode 100644 index b0c8c050b..000000000 --- a/packages/integrations/src/npm/npm-integration.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; - -import type { IntegrationTestingInput } from "../base/integration"; -import { Integration } from "../base/integration"; -import { TestConnectionError } from "../base/test-connection/test-connection-error"; -import type { TestingResult } from "../base/test-connection/test-connection-service"; -import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration"; -import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration"; -import type { ReleaseResponse } from "../interfaces/releases-providers/releases-providers-types"; -import { releasesResponseSchema } from "./npm-schemas"; - -export class NPMIntegration extends Integration implements ReleasesProviderIntegration { - protected async testingAsync(input: IntegrationTestingInput): Promise { - const response = await input.fetchAsync(this.url("/")); - - if (!response.ok) { - return TestConnectionError.StatusResult(response); - } - - return { - success: true, - }; - } - - public async getLatestMatchingReleaseAsync(identifier: string, versionRegex?: string): Promise { - if (!identifier) return { success: false, error: { code: "invalidIdentifier" } }; - - const releasesResponse = await fetchWithTrustedCertificatesAsync(this.url(`/${encodeURIComponent(identifier)}`)); - if (!releasesResponse.ok) { - return { success: false, error: { code: "unexpected", message: releasesResponse.statusText } }; - } - - const releasesResponseJson: unknown = await releasesResponse.json(); - const { data, success, error } = releasesResponseSchema.safeParse(releasesResponseJson); - if (!success) { - return { - success: false, - error: { - code: "unexpected", - message: releasesResponseJson ? JSON.stringify(releasesResponseJson, null, 2) : error.message, - }, - }; - } - - const formattedReleases = data.time.map((tag) => ({ - ...tag, - releaseUrl: `https://www.npmjs.com/package/${encodeURIComponent(data.name)}/v/${encodeURIComponent(tag.latestRelease)}`, - releaseDescription: data.versions[tag.latestRelease]?.description ?? "", - })); - - const latestRelease = getLatestRelease(formattedReleases, versionRegex); - if (!latestRelease) return { success: false, error: { code: "noMatchingVersion" } }; - - return { success: true, data: latestRelease }; - } -} diff --git a/packages/integrations/src/npm/npm-schemas.ts b/packages/integrations/src/npm/npm-schemas.ts deleted file mode 100644 index 5d21734e2..000000000 --- a/packages/integrations/src/npm/npm-schemas.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { z } from "zod/v4"; - -export const releasesResponseSchema = z.object({ - time: z - .record( - z.string(), - z.string().transform((value) => new Date(value)), - ) - .transform((version) => - Object.entries(version).map(([key, value]) => ({ - latestRelease: key, - latestReleaseAt: value, - })), - ), - versions: z.record(z.string(), z.object({ description: z.string() })), - name: z.string(), -}); diff --git a/packages/integrations/src/overseerr/overseerr-integration.ts b/packages/integrations/src/overseerr/overseerr-integration.ts index 643d2ac60..a4429bd27 100644 --- a/packages/integrations/src/overseerr/overseerr-integration.ts +++ b/packages/integrations/src/overseerr/overseerr-integration.ts @@ -132,11 +132,14 @@ export class OverseerrIntegration }, ); - const allRequests = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/request", { take: 20 }), { - headers: { - "X-Api-Key": this.getSecretValue("apiKey"), + const allRequests = await fetchWithTrustedCertificatesAsync( + this.url("/api/v1/request", { take: 20, sort: "modified" }), + { + headers: { + "X-Api-Key": this.getSecretValue("apiKey"), + }, }, - }); + ); const pendingResults = (await getRequestsSchema.parseAsync(await pendingRequests.json())).results; const allResults = (await getRequestsSchema.parseAsync(await allRequests.json())).results; diff --git a/packages/integrations/src/patchmon/patchmon-integration.ts b/packages/integrations/src/patchmon/patchmon-integration.ts new file mode 100644 index 000000000..2a9c4dc5a --- /dev/null +++ b/packages/integrations/src/patchmon/patchmon-integration.ts @@ -0,0 +1,58 @@ +import { ParseError, ResponseError } from "@homarr/common/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; + +import type { IntegrationTestingInput } from "../base/integration"; +import { Integration } from "../base/integration"; +import { TestConnectionError } from "../base/test-connection/test-connection-error"; +import type { TestingResult } from "../base/test-connection/test-connection-service"; +import type { PatchMonStats } from "./patchmon-types"; +import { mapPatchMonStats, parsePatchMonStatsResponseAsync } from "./patchmon-types"; + +const STATS_REQUEST_TIMEOUT_MS = 10_000; + +export class PatchMonIntegration extends Integration { + protected async testingAsync(input: IntegrationTestingInput): Promise { + const response = await input.fetchAsync(this.url("/api/v1/gethomepage/stats"), { + headers: this.getAuthHeaders(), + }); + + if (!response.ok) { + return TestConnectionError.StatusResult(response); + } + + try { + await parsePatchMonStatsResponseAsync(response); + } catch (error) { + if (error instanceof ParseError) { + return TestConnectionError.ParseResult(error); + } + + throw error; + } + + return { success: true }; + } + + public async getStatsAsync(): Promise { + const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/gethomepage/stats"), { + headers: this.getAuthHeaders(), + timeout: STATS_REQUEST_TIMEOUT_MS, + }); + + if (!response.ok) { + throw new ResponseError(response); + } + + const data = await parsePatchMonStatsResponseAsync(response); + + return mapPatchMonStats(data); + } + + private getAuthHeaders(): Record { + const credentials = Buffer.from( + `${this.getSecretValue("patchmonApiKey")}:${this.getSecretValue("patchmonApiSecret")}`, + ).toString("base64"); + + return { Authorization: `Basic ${credentials}` }; + } +} diff --git a/packages/integrations/src/patchmon/patchmon-types.ts b/packages/integrations/src/patchmon/patchmon-types.ts new file mode 100644 index 000000000..1f912d6c9 --- /dev/null +++ b/packages/integrations/src/patchmon/patchmon-types.ts @@ -0,0 +1,88 @@ +import { ParseError } from "@homarr/common/server"; +import { z } from "zod/v4"; + +const PATCHMON_STATS_PARSE_ERROR_MESSAGE = "Invalid PatchMon stats response"; + +export const patchmonOsDistributionEntrySchema = z.object({ + name: z.string(), + count: z.number(), + os_type: z.string().optional(), + os_version: z.string().optional(), +}); + +export const patchmonStatsResponseSchema = z.object({ + total_hosts: z.number(), + hosts_needing_updates: z.number(), + security_updates: z.number(), + up_to_date_hosts: z.number(), + hosts_with_security_updates: z.number(), + recent_updates_24h: z.number(), + total_outdated_packages: z.number(), + total_repos: z.number(), + last_updated: z.string(), + os_distribution: z.array(patchmonOsDistributionEntrySchema).default([]), +}); + +export interface PatchMonOsDistributionEntry { + name: string; + count: number; + osType?: string; + osVersion?: string; +} + +export interface PatchMonStats { + totalHosts: number; + hostsNeedingUpdates: number; + securityUpdates: number; + upToDateHosts: number; + hostsWithSecurityUpdates: number; + recentUpdates24h: number; + totalOutdatedPackages: number; + totalRepos: number; + lastUpdated: string; + osDistribution: PatchMonOsDistributionEntry[]; +} + +const mapOsDistributionEntry = ( + entry: z.infer, +): PatchMonOsDistributionEntry => ({ + name: entry.name, + count: entry.count, + osType: entry.os_type, + osVersion: entry.os_version, +}); + +export const parsePatchMonStatsResponseAsync = async ( + response: { json: () => Promise }, +): Promise> => { + let json: unknown; + try { + json = await response.json(); + } catch (error) { + throw new ParseError(PATCHMON_STATS_PARSE_ERROR_MESSAGE, { + cause: error instanceof Error ? error : new Error(String(error)), + }); + } + + const parseResult = await patchmonStatsResponseSchema.safeParseAsync(json); + if (!parseResult.success) { + throw new ParseError(PATCHMON_STATS_PARSE_ERROR_MESSAGE, { cause: parseResult.error }); + } + + return parseResult.data; +}; + +export const mapPatchMonStats = (data: z.infer): PatchMonStats => ({ + totalHosts: data.total_hosts, + hostsNeedingUpdates: data.hosts_needing_updates, + securityUpdates: data.security_updates, + upToDateHosts: data.up_to_date_hosts, + hostsWithSecurityUpdates: data.hosts_with_security_updates, + recentUpdates24h: data.recent_updates_24h, + totalOutdatedPackages: data.total_outdated_packages, + totalRepos: data.total_repos, + lastUpdated: data.last_updated, + osDistribution: [...data.os_distribution] + .map(mapOsDistributionEntry) + .sort((a, b) => b.count - a.count), +}); diff --git a/packages/integrations/src/patchmon/test/patchmon-integration.spec.ts b/packages/integrations/src/patchmon/test/patchmon-integration.spec.ts new file mode 100644 index 000000000..74924aecd --- /dev/null +++ b/packages/integrations/src/patchmon/test/patchmon-integration.spec.ts @@ -0,0 +1,167 @@ +// @vitest-environment node +import { ParseError } from "@homarr/common/server"; +import { Response } from "undici"; +import { beforeEach, describe, expect, test, vi } from "vitest"; + +vi.hoisted(() => { + process.env.SKIP_ENV_VALIDATION = "true"; + process.env.SECRET_ENCRYPTION_KEY = "ff3f4f7ce30e870c9630de9e5d244ffa81101a24ed0dfe5f064beb53a7e684f1"; +}); + +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; + +import { IntegrationParseError } from "../../base/errors/parse/integration-parse-error"; +import type { IntegrationSecret } from "../../base/types"; +import { PatchMonIntegration } from "../patchmon-integration"; + +vi.mock("@homarr/core/infrastructure/http", () => ({ + fetchWithTrustedCertificatesAsync: vi.fn(), +})); + +const TEST_URL = "https://patchmon.example.com"; +const TEST_API_KEY = "test-api-key"; +const TEST_API_SECRET = "test-api-secret"; + +const mockFetch = vi.mocked(fetchWithTrustedCertificatesAsync); + +const sampleStatsResponse = { + total_hosts: 42, + hosts_needing_updates: 15, + security_updates: 23, + up_to_date_hosts: 27, + hosts_with_security_updates: 8, + recent_updates_24h: 34, + total_outdated_packages: 156, + total_repos: 12, + last_updated: "2025-10-11T12:34:56.789Z", + os_distribution: [ + { name: "Debian", count: 12, os_type: "linux", os_version: "12" }, + { name: "Ubuntu", count: 20, os_type: "linux", os_version: "22.04" }, + { name: "Rocky Linux", count: 10, os_type: "linux", os_version: "9" }, + ], +}; + +const createIntegration = (decryptedSecrets: IntegrationSecret[] = []) => + new PatchMonIntegration({ + id: "test-patchmon", + name: "Test PatchMon", + url: TEST_URL, + externalUrl: null, + decryptedSecrets: + decryptedSecrets.length > 0 + ? decryptedSecrets + : [ + { kind: "patchmonApiKey", value: TEST_API_KEY }, + { kind: "patchmonApiSecret", value: TEST_API_SECRET }, + ], + }); + +beforeEach(() => { + mockFetch.mockReset(); +}); + +describe("PatchMonIntegration getStatsAsync", () => { + test("maps API response fields to camelCase stats", async () => { + mockFetch.mockResolvedValue( + new Response(JSON.stringify(sampleStatsResponse), { + status: 200, + headers: { "content-type": "application/json" }, + }) as unknown as Awaited>, + ); + + const stats = await createIntegration().getStatsAsync(); + + expect(stats).toStrictEqual({ + totalHosts: 42, + hostsNeedingUpdates: 15, + securityUpdates: 23, + upToDateHosts: 27, + hostsWithSecurityUpdates: 8, + recentUpdates24h: 34, + totalOutdatedPackages: 156, + totalRepos: 12, + lastUpdated: "2025-10-11T12:34:56.789Z", + osDistribution: [ + { name: "Ubuntu", count: 20, osType: "linux", osVersion: "22.04" }, + { name: "Debian", count: 12, osType: "linux", osVersion: "12" }, + { name: "Rocky Linux", count: 10, osType: "linux", osVersion: "9" }, + ], + }); + }); + + test("defaults os distribution to empty array when absent", async () => { + const { os_distribution: _osDistribution, ...responseWithoutOs } = sampleStatsResponse; + mockFetch.mockResolvedValue( + new Response(JSON.stringify(responseWithoutOs), { + status: 200, + headers: { "content-type": "application/json" }, + }) as unknown as Awaited>, + ); + + const stats = await createIntegration().getStatsAsync(); + + expect(stats.osDistribution).toStrictEqual([]); + }); + + test("throws ParseError when API response is not valid JSON", async () => { + mockFetch.mockResolvedValue( + new Response("not-json", { + status: 200, + headers: { "content-type": "application/json" }, + }) as unknown as Awaited>, + ); + + await expect(createIntegration().getStatsAsync()).rejects.toSatisfy((error) => { + if (!(error instanceof IntegrationParseError)) return false; + + const cause = error.cause; + return cause instanceof ParseError && cause.message.includes("Invalid PatchMon stats response"); + }); + }); + + test("throws ParseError when API response does not match schema", async () => { + mockFetch.mockResolvedValue( + new Response(JSON.stringify({ invalid: true }), { + status: 200, + headers: { "content-type": "application/json" }, + }) as unknown as Awaited>, + ); + + await expect(createIntegration().getStatsAsync()).rejects.toSatisfy((error) => { + if (!(error instanceof IntegrationParseError)) return false; + + const cause = error.cause; + return cause instanceof ParseError && cause.message.includes("Invalid PatchMon stats response"); + }); + }); + + test("throws when API returns an error", async () => { + mockFetch.mockResolvedValue( + new Response("Unauthorized", { + status: 401, + headers: { "content-type": "text/plain" }, + }) as unknown as Awaited>, + ); + + await expect(createIntegration().getStatsAsync()).rejects.toThrow(); + }); +}); + +describe("PatchMonIntegration authentication", () => { + test("sends a Basic auth header with api key and secret", async () => { + mockFetch.mockResolvedValue( + new Response(JSON.stringify(sampleStatsResponse), { + status: 200, + headers: { "content-type": "application/json" }, + }) as unknown as Awaited>, + ); + + await createIntegration().getStatsAsync(); + + const expected = `Basic ${Buffer.from(`${TEST_API_KEY}:${TEST_API_SECRET}`).toString("base64")}`; + const [url, requestInit] = mockFetch.mock.calls[0] ?? []; + expect(String(url)).toContain("/api/v1/gethomepage/stats"); + expect(requestInit?.headers).toMatchObject({ Authorization: expected }); + expect(requestInit?.timeout).toBe(10_000); + }); +}); diff --git a/packages/integrations/src/quay/quay-integration.ts b/packages/integrations/src/quay/quay-integration.ts deleted file mode 100644 index 21cbf797e..000000000 --- a/packages/integrations/src/quay/quay-integration.ts +++ /dev/null @@ -1,97 +0,0 @@ -import type { RequestInit, Response } from "undici"; - -import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; -import { createLogger } from "@homarr/core/infrastructure/logs"; - -import type { IntegrationTestingInput } from "../base/integration"; -import { Integration } from "../base/integration"; -import { TestConnectionError } from "../base/test-connection/test-connection-error"; -import type { TestingResult } from "../base/test-connection/test-connection-service"; -import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration"; -import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration"; -import type { - ReleaseProviderResponse, - ReleaseResponse, -} from "../interfaces/releases-providers/releases-providers-types"; -import { releasesResponseSchema } from "./quay-schemas"; - -const logger = createLogger({ module: "quayIntegration" }); - -export class QuayIntegration extends Integration implements ReleasesProviderIntegration { - private async withHeadersAsync(callback: (headers: RequestInit["headers"]) => Promise): Promise { - if (!this.hasSecretValue("personalAccessToken")) return await callback(undefined); - - return await callback({ - Authorization: `token ${this.getSecretValue("personalAccessToken")}`, - }); - } - - protected async testingAsync(input: IntegrationTestingInput): Promise { - const response = await this.withHeadersAsync(async (headers) => { - return await input.fetchAsync(this.url("/api/v1/discovery"), { - headers, - }); - }); - - if (!response.ok) { - return TestConnectionError.StatusResult(response); - } - - return { - success: true, - }; - } - - private parseIdentifier(identifier: string) { - const [owner, name] = identifier.split("/"); - if (!owner || !name) { - logger.warn("Invalid identifier format. Expected 'owner/name' for identifier", { - identifier, - }); - return null; - } - return { owner, name }; - } - - public async getLatestMatchingReleaseAsync(identifier: string, versionRegex?: string): Promise { - const parsedIdentifier = this.parseIdentifier(identifier); - if (!parsedIdentifier) return { success: false, error: { code: "invalidIdentifier" } }; - - const { owner, name } = parsedIdentifier; - - const releasesResponse = await this.withHeadersAsync(async (headers) => { - return await fetchWithTrustedCertificatesAsync( - this.url( - `/api/v1/repository/${encodeURIComponent(owner)}/${encodeURIComponent(name)}?includeTags=true&includeStats=true`, - ), - { - headers, - }, - ); - }); - if (!releasesResponse.ok) { - return { success: false, error: { code: "unexpected", message: releasesResponse.statusText } }; - } - - const releasesResponseJson: unknown = await releasesResponse.json(); - const { data, success, error } = releasesResponseSchema.safeParse(releasesResponseJson); - if (!success) { - return { success: false, error: { code: "unexpected", message: error.message } }; - } - - const releasesProviderResponse = Object.entries(data.tags).reduce((acc, [_, tag]) => { - if (!tag.name || !tag.last_modified) return acc; - acc.push({ - latestRelease: tag.name, - latestReleaseAt: new Date(tag.last_modified), - releaseUrl: `https://quay.io/repository/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/tag/${encodeURIComponent(tag.name)}`, - }); - return acc; - }, []); - - const latestRelease = getLatestRelease(releasesProviderResponse, versionRegex); - if (!latestRelease) return { success: false, error: { code: "noMatchingVersion" } }; - - return { success: true, data: { projectDescription: data.description, ...latestRelease } }; - } -} diff --git a/packages/integrations/src/quay/quay-schemas.ts b/packages/integrations/src/quay/quay-schemas.ts deleted file mode 100644 index e24fe2d83..000000000 --- a/packages/integrations/src/quay/quay-schemas.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { z } from "zod/v4"; - -export const releasesResponseSchema = z.object({ - description: z.string().optional(), - tags: z.record( - z.string(), - z.object({ - name: z.string(), - last_modified: z.string(), - }), - ), -}); diff --git a/packages/integrations/src/search-ch/search-ch-integration.ts b/packages/integrations/src/search-ch/search-ch-integration.ts deleted file mode 100644 index cbaf8e80d..000000000 --- a/packages/integrations/src/search-ch/search-ch-integration.ts +++ /dev/null @@ -1,141 +0,0 @@ -import z from "zod"; - -import { ResponseError } from "@homarr/common/server"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; - -import type { IntegrationTestingInput } from "../base/integration"; -import { Integration } from "../base/integration"; -import { TestConnectionError } from "../base/test-connection/test-connection-error"; -import type { TestingResult } from "../base/test-connection/test-connection-service"; -import type { TimetableIntegration } from "../interfaces/timetable/timetable-integration"; -import type { Station, Timetable, TimetableOptions } from "../interfaces/timetable/timetable-types"; - -const supportedStationTypes = ["bus", "tram", "train", "ship", "cablecar", "funicular", "chairlift"]; - -export class SearchChIntegration extends Integration implements TimetableIntegration { - protected async testingAsync(input: IntegrationTestingInput): Promise { - const response = await input.fetchAsync(this.url("/timetable/api/completion.json", { term: "Zürich" })); - - if (!response.ok) { - return TestConnectionError.StatusResult(response); - } - - return { - success: true, - }; - } - - /** - * Searches for stations matching the given query. - * @param query The search query for station names. - * @returns A list of stations matching the search query. - * @docs https://search.ch/fahrplan/api/help - */ - public async searchStationsAsync(query: string): Promise { - const response = await fetchWithTrustedCertificatesAsync( - this.url("/timetable/api/completion.json", { - term: query, - show_ids: 1, - nofavorites: 1, - }), - ); - - if (!response.ok) { - throw new ResponseError(response); - } - - const data = await searchSchema.parseAsync(await response.json()); - - return ( - data - // Remove completions like addresses, we only want stations. - .filter((item) => supportedStationTypes.some((type) => item.iconclass.endsWith(type))) - .map((item) => (item.id !== undefined ? { id: item.id, name: item.label } : null)) - .filter((item) => item !== null) - ); - } - - /** - * Gets the departure timetable for a given station. - * @param options Timetable options including station ID and limit. - * @returns timetable for the given station. - * @docs https://search.ch/fahrplan/api/help - */ - public async getTimetableAsync(options: TimetableOptions): Promise { - const now = new Date(); - // The following format is used [01.01.2000, 23:59:00]. The API allows usage of DD.MM.YYYY format and HH:mm format - const [date, time] = now.toLocaleString("en-CH", { timeZone: "Europe/Zurich" }).split(", ") as [string, string]; - - const response = await fetchWithTrustedCertificatesAsync( - this.url("/timetable/api/stationboard.json", { - stop: options.stationId, - limit: options.limit, - show_delays: 1, - show_tracks: 1, - date, - time: time.substring(0, 5), // Only keep the HH:mm part of the time, as that's what the API expects - }), - ); - - if (!response.ok) { - throw new ResponseError(response); - } - - const data = await timetableSchema.parseAsync(await response.json()); - - return { - stationId: options.stationId, - timestamp: now, - entries: data.connections.map((connection) => { - // Color is in format A35~F10~ for foreground and background color. Some only have ~~ and therefore no color - const color = connection.color.split("~")[0]; - - return { - timestamp: connection.time, - line: connection.line - ? { - name: connection.line, - color: color && color.length >= 1 ? `#${color}` : null, - } - : null, - location: connection.terminal.name, - delay: connection.dep_delay, - platform: connection.track - ? { - // If the track name contains "!", it indicates a change in the track - // so we remove the "!" from the name and set hasChanged to true - name: connection.track.replace("!", ""), - hasChanged: connection.track.includes("!"), - } - : null, - }; - }), - }; - } -} - -const timetableSchema = z.object({ - connections: z.array( - z.object({ - time: z.string().transform((str) => new Date(str)), - line: z.string().optional(), - color: z.string(), - terminal: z.object({ - name: z.string(), - }), - track: z.string().optional(), - dep_delay: z - .string() - .optional() - .transform((str) => (str ? parseInt(str) : 0)), - }), - ), -}); - -const searchSchema = z.array( - z.object({ - id: z.string().optional(), - label: z.string(), - iconclass: z.string(), // This is in format sl-icon-type-{type} - }), -); diff --git a/packages/integrations/src/synology/synology-client.ts b/packages/integrations/src/synology/synology-client.ts new file mode 100644 index 000000000..cc853e2c9 --- /dev/null +++ b/packages/integrations/src/synology/synology-client.ts @@ -0,0 +1,404 @@ +import type { Headers } from "undici"; +import type { z } from "zod"; + +import { ResponseError } from "@homarr/common/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; +import { createLogger } from "@homarr/core/infrastructure/logs"; + +import type { SessionStore } from "../base/session-store"; +import type { SynologyDiskRecord, SynologyVolumeRecord } from "./synology-types"; +import { + AUTH_API_NAME, + INFO_API_NAME, + REQUEST_TIMEOUT_MS, + SESSION_ERROR_CODES, + synologyApiInfoResponseSchema, + synologyAuthResponseSchema, + synologyEnvelopeSchema, + synologyLegacyStorageDataSchema, + synologySmartInfoDataSchema, + synologyStorageLoadInfoDataSchema, + synologyStorageV2DataSchema, + synologySystemInfoDataSchema, + synologySystemStatusDataSchema, + synologyUpgradeCheckDataSchema, + synologyUpgradeStatusDataSchema, + synologyUtilizationDataSchema, +} from "./synology-types"; + +const logger = createLogger({ module: "synologyClient" }); + +const AUTH_SESSION_NAME = "DownloadStation"; + +type ApiDefinition = { + cgiPath: string; + maxVersion: number; +}; + +type StoredSession = { + cookieHeader: string; +}; + +type SynologyClientOptions = { + integrationId: string; + baseUrl: string; + username: string; + password: string; + sessionStore: SessionStore; +}; + +const apiDefinitionCache = new Map(); + +export class SynologyClient { + private readonly integrationId: string; + private readonly baseUrl: string; + private readonly username: string; + private readonly password: string; + private readonly sessionStore: SessionStore; + + constructor(options: SynologyClientOptions) { + this.integrationId = options.integrationId; + this.baseUrl = options.baseUrl.replace(/\/$/, ""); + this.username = options.username; + this.password = options.password; + this.sessionStore = options.sessionStore; + } + + public async getSystemInfoAsync() { + const response = await this.requestAsync("SYNO.Core.System", { method: "info" }); + return synologySystemInfoDataSchema.parse(response.data); + } + + public async getUtilizationAsync() { + const response = await this.requestAsync("SYNO.Core.System.Utilization", { method: "get" }); + return synologyUtilizationDataSchema.parse(response.data); + } + + public async getStorageVolumesAsync(): Promise { + try { + const storageV2Response = await this.requestAsync("SYNO.Core.System", { + method: "info", + extraParams: { type: "storage_v2" }, + }); + const storageV2 = synologyStorageV2DataSchema.parse(storageV2Response.data); + const volumesFromV2 = this.mapStorageV2Volumes(storageV2); + if (volumesFromV2.length > 0) { + return volumesFromV2; + } + } catch (error) { + logger.debug("storage_v2 unavailable, falling back to legacy storage info", { + integrationId: this.integrationId, + error, + }); + } + + const legacyResponse = await this.requestAsync("SYNO.Core.System", { + method: "info", + extraParams: { type: "storage" }, + }); + const legacyStorage = synologyLegacyStorageDataSchema.parse(legacyResponse.data); + return this.mapLegacyVolumes(legacyStorage.vol_info ?? []); + } + + public async getStorageLoadInfoAsync() { + const response = await this.requestOptionalAsync("SYNO.Storage.CGI.Storage", { method: "load_info" }); + if (!response) { + return null; + } + return synologyStorageLoadInfoDataSchema.parse(response.data); + } + + public async getSmartInfoAsync() { + const response = await this.requestOptionalAsync("SYNO.Storage.CGI.Smart", { method: "get_smart_info" }); + if (!response) { + return null; + } + return synologySmartInfoDataSchema.parse(response.data); + } + + public async getSystemStatusAsync() { + const response = await this.requestOptionalAsync("SYNO.Core.System.Status", { method: "get" }); + if (!response) { + return null; + } + return synologySystemStatusDataSchema.parse(response.data); + } + + public async getUpgradeStatusAsync() { + const response = await this.requestOptionalAsync("SYNO.Core.Upgrade", { method: "status" }); + if (!response) { + return null; + } + return synologyUpgradeStatusDataSchema.parse(response.data); + } + + public async getUpgradeCheckAsync() { + const response = await this.requestOptionalAsync("SYNO.Core.Upgrade.Server", { method: "check" }); + if (!response) { + return null; + } + return synologyUpgradeCheckDataSchema.parse(response.data); + } + + public async testConnectionAsync() { + await this.getSystemInfoAsync(); + } + + private mapLegacyVolumes(volumes: z.infer["vol_info"]) { + return (volumes ?? []) + .map((volume) => this.mapVolumeRecord(volume.name, volume.used_size, volume.total_size, volume.status)) + .filter((volume): volume is SynologyVolumeRecord => volume !== null); + } + + private mapStorageV2Volumes(storage: z.infer) { + return (storage.volumes ?? []) + .map((volume) => { + const name = volume.id ?? volume.vol_path ?? volume.display_name; + if (!name) { + return null; + } + return this.mapVolumeRecord(name, volume.size?.used, volume.size?.total, volume.status, volume.display_name); + }) + .filter((volume): volume is SynologyVolumeRecord => volume !== null); + } + + private mapVolumeRecord( + name: string, + usedValue: number | string | undefined, + totalValue: number | string | undefined, + status: string | undefined, + displayName?: string, + ): SynologyVolumeRecord | null { + const usedBytes = parseNumericValue(usedValue); + const totalBytes = parseNumericValue(totalValue); + if (usedBytes === null || totalBytes === null || totalBytes <= 0) { + return null; + } + + return { + name, + displayName: displayName && displayName !== name ? displayName : undefined, + usedBytes, + totalBytes, + status, + }; + } + + public mapDisksFromLoadInfo(loadInfo: z.infer): SynologyDiskRecord[] { + return (loadInfo.disks ?? []).map((disk) => ({ + identifier: disk.id ?? disk.name ?? disk.display_name ?? "unknown-disk", + name: disk.display_name ?? disk.name ?? disk.id ?? "unknown-disk", + status: disk.smart_status ?? disk.status, + temperature: parseOptionalNumericValue(disk.temp ?? disk.temperature), + volumeName: disk.volume_id ?? disk.vol_path, + })); + } + + public mapDisksFromSmartInfo(smartInfo: z.infer): SynologyDiskRecord[] { + const disks = smartInfo.disks ?? smartInfo.items ?? []; + return disks.map((disk) => ({ + identifier: disk.disk_id ?? disk.id ?? disk.name ?? "unknown-disk", + name: disk.name ?? disk.disk_id ?? disk.id ?? "unknown-disk", + status: disk.overall_status ?? disk.status, + temperature: parseOptionalNumericValue(disk.temp ?? disk.temperature), + volumeName: undefined, + })); + } + + private async requestOptionalAsync( + apiName: string, + options: { method: string; extraParams?: Record }, + ) { + try { + return await this.requestAsync(apiName, options); + } catch (error) { + logger.debug("Optional Synology API request failed", { + integrationId: this.integrationId, + apiName, + method: options.method, + error, + }); + return null; + } + } + + private async requestAsync( + apiName: string, + options: { method: string; extraParams?: Record }, + ): Promise> { + return await this.withAuthenticatedSessionAsync(async (session) => { + const apiDefinition = await this.getApiDefinitionAsync(apiName); + const requestUrl = new URL(`${this.baseUrl}/webapi/${apiDefinition.cgiPath}`); + requestUrl.searchParams.set("api", apiName); + requestUrl.searchParams.set("version", String(apiDefinition.maxVersion)); + requestUrl.searchParams.set("method", options.method); + for (const [key, value] of Object.entries(options.extraParams ?? {})) { + requestUrl.searchParams.set(key, value); + } + + const response = await fetchWithTrustedCertificatesAsync(requestUrl, { + headers: { + Cookie: session.cookieHeader, + "User-Agent": "Homarr", + }, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + + if (!response.ok) { + throw new ResponseError(response); + } + + const payload = synologyEnvelopeSchema.parse(await response.json()); + if (!payload.success) { + const errorCode = payload.error?.code; + if (errorCode !== undefined && SESSION_ERROR_CODES.has(errorCode)) { + throw new SynologySessionError(errorCode); + } + throw new ResponseError({ + status: 502, + url: requestUrl.toString(), + }); + } + + return payload; + }); + } + + private async withAuthenticatedSessionAsync(callback: (session: StoredSession) => Promise): Promise { + const storedSession = await this.sessionStore.getAsync(); + if (storedSession) { + try { + return await callback(storedSession); + } catch (error) { + if (!(error instanceof SynologySessionError)) { + throw error; + } + logger.debug("Synology session expired, re-authenticating", { + integrationId: this.integrationId, + errorCode: error.errorCode, + }); + await this.sessionStore.clearAsync(); + } + } + + const session = await this.loginAsync(); + await this.sessionStore.setAsync(session); + return await callback(session); + } + + private async loginAsync(): Promise { + const authDefinition = await this.getApiDefinitionAsync(AUTH_API_NAME); + const loginUrl = new URL(`${this.baseUrl}/webapi/${authDefinition.cgiPath}`); + loginUrl.searchParams.set("api", AUTH_API_NAME); + loginUrl.searchParams.set("version", String(authDefinition.maxVersion)); + loginUrl.searchParams.set("method", "login"); + const safeLoginUrl = loginUrl.toString(); + + const response = await fetchWithTrustedCertificatesAsync(loginUrl, { + method: "POST", + headers: { + "User-Agent": "Homarr", + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + account: this.username, + passwd: this.password, + session: AUTH_SESSION_NAME, + format: "cookie", + }).toString(), + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + + if (!response.ok) { + throw new ResponseError({ status: response.status, url: safeLoginUrl }); + } + + const payload = synologyAuthResponseSchema.parse(await response.json()); + if (!payload.success) { + throw new ResponseError({ + status: 401, + url: safeLoginUrl, + }); + } + + const cookieHeader = extractCookieHeader(response.headers); + if (!cookieHeader) { + throw new ResponseError({ + status: 401, + url: safeLoginUrl, + }); + } + + return { cookieHeader }; + } + + private async getApiDefinitionAsync(apiName: string): Promise { + const cacheKey = `${this.integrationId}:${apiName}`; + const cachedDefinition = apiDefinitionCache.get(cacheKey); + if (cachedDefinition) { + return cachedDefinition; + } + + const infoUrl = new URL(`${this.baseUrl}/webapi/query.cgi`); + infoUrl.searchParams.set("api", INFO_API_NAME); + infoUrl.searchParams.set("version", "1"); + infoUrl.searchParams.set("method", "query"); + infoUrl.searchParams.set("query", apiName); + + const response = await fetchWithTrustedCertificatesAsync(infoUrl, { + headers: { + "User-Agent": "Homarr", + }, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + + if (!response.ok) { + throw new ResponseError(response); + } + + const payload = synologyApiInfoResponseSchema.parse(await response.json()); + const apiDefinition = payload.data?.[apiName]; + if (!payload.success || !apiDefinition) { + throw new ResponseError({ + status: 502, + url: infoUrl.toString(), + }); + } + + const definition = { + cgiPath: apiDefinition.path, + maxVersion: apiDefinition.maxVersion, + }; + apiDefinitionCache.set(cacheKey, definition); + return definition; + } +} + +class SynologySessionError extends Error { + constructor(public readonly errorCode: number) { + super(`Synology session error ${String(errorCode)}`); + this.name = SynologySessionError.name; + } +} + +function extractCookieHeader(headers: Headers): string | null { + const cookies = headers.getSetCookie(); + if (cookies.length === 0) { + return null; + } + + return cookies.map((cookie) => cookie.split(";")[0]).join("; "); +} + +function parseNumericValue(value: number | string | undefined): number | null { + if (value === undefined) { + return null; + } + const parsed = typeof value === "number" ? value : Number.parseFloat(value); + return Number.isFinite(parsed) ? parsed : null; +} + +function parseOptionalNumericValue(value: number | string | undefined): number | null { + const parsed = parseNumericValue(value); + return parsed === null ? null : parsed; +} diff --git a/packages/integrations/src/synology/synology-integration.ts b/packages/integrations/src/synology/synology-integration.ts new file mode 100644 index 000000000..90c49f4bf --- /dev/null +++ b/packages/integrations/src/synology/synology-integration.ts @@ -0,0 +1,323 @@ +import type { z } from "zod"; + +import { createLogger } from "@homarr/core/infrastructure/logs"; + +import { HandleIntegrationErrors } from "../base/errors/decorator"; +import type { IntegrationTestingInput } from "../base/integration"; +import { Integration } from "../base/integration"; +import { createSessionStore } from "../base/session-store"; +import type { TestingResult } from "../base/test-connection/test-connection-service"; +import type { ISystemHealthMonitoringIntegration } from "../interfaces/health-monitoring/health-monitoring-integration"; +import type { SystemHealthMonitoring } from "../interfaces/health-monitoring/health-monitoring-types"; +import { SynologyClient } from "./synology-client"; +import type { SynologyDiskRecord, SynologyVolumeRecord } from "./synology-types"; +import type { synologySystemInfoDataSchema, synologyUtilizationDataSchema } from "./synology-types"; + +const logger = createLogger({ module: "synologyIntegration" }); + +type StoredSession = { + cookieHeader: string; +}; + +@HandleIntegrationErrors([]) +export class SynologyIntegration extends Integration implements ISystemHealthMonitoringIntegration { + private readonly sessionStore = createSessionStore(this.integration); + private client?: SynologyClient; + + protected async testingAsync(_input: IntegrationTestingInput): Promise { + await this.getClient().testConnectionAsync(); + return { success: true }; + } + + public async getSystemInfoAsync(): Promise { + const client = this.getClient(); + + const [systemInfo, utilization, volumes] = await Promise.all([ + client.getSystemInfoAsync(), + client.getUtilizationAsync(), + client.getStorageVolumesAsync(), + ]); + + const [storageLoadInfoResult, smartInfoResult, systemStatusResult, upgradeStatusResult, upgradeCheckResult] = + await Promise.allSettled([ + client.getStorageLoadInfoAsync(), + client.getSmartInfoAsync(), + client.getSystemStatusAsync(), + client.getUpgradeStatusAsync(), + client.getUpgradeCheckAsync(), + ]); + + const storageLoadInfo = getSettledValue(storageLoadInfoResult); + const smartInfo = getSettledValue(smartInfoResult); + const systemStatus = getSettledValue(systemStatusResult); + const upgradeStatus = getSettledValue(upgradeStatusResult); + const upgradeCheck = getSettledValue(upgradeCheckResult); + + const diskRecords = mergeDiskRecords( + storageLoadInfo ? client.mapDisksFromLoadInfo(storageLoadInfo) : [], + smartInfo ? client.mapDisksFromSmartInfo(smartInfo) : [], + ); + + const memory = mapMemoryBytes(utilization); + const network = mapNetworkTraffic(utilization); + const loadAverage = mapLoadAverage(utilization); + const smart = mapVolumeSmartEntries(volumes, diskRecords); + const rebootRequired = Boolean(systemStatus?.reboot_required ?? upgradeStatus?.reboot_required ?? false); + const availablePkgUpdates = upgradeCheck?.update_count ?? (upgradeCheck?.available ? 1 : 0); + + return { + version: mapVersion(systemInfo), + cpuModelName: systemInfo.model ?? "Unknown CPU", + cpuUtilization: mapCpuUtilization(utilization), + memUsedInBytes: memory.usedBytes, + memAvailableInBytes: memory.availableBytes, + uptime: mapUptimeSeconds(systemInfo), + network, + loadAverage, + rebootRequired, + availablePkgUpdates, + cpuTemp: mapCpuTemperature(systemInfo), + fileSystem: mapFileSystemEntries(volumes), + smart, + gpu: [], + }; + } + + public async listStorageVolumesAsync(): Promise<{ value: string; label: string }[]> { + const volumes = await this.getClient().getStorageVolumesAsync(); + const { id: integrationId, name: integrationName } = this.integration; + return volumes.map((volume) => ({ + value: `${integrationId}:${volume.name}`, + label: `${volume.displayName ?? volume.name} (${integrationName})`, + })); + } + + private getClient(): SynologyClient { + return (this.client ??= new SynologyClient({ + integrationId: this.integration.id, + baseUrl: this.integration.url, + username: this.getSecretValue("username"), + password: this.getSecretValue("password"), + sessionStore: this.sessionStore, + })); + } +} + +function getSettledValue(result: PromiseSettledResult): T | null { + if (result.status === "rejected") { + logger.debug("Optional Synology API call failed", { reason: result.reason }); + return null; + } + return result.value; +} + +function mapVersion(systemInfo: z.infer): string { + if (systemInfo.version_string) { + return systemInfo.version_string; + } + if (systemInfo.firmware_ver) { + return systemInfo.firmware_ver; + } + if (systemInfo.version !== undefined) { + return String(systemInfo.version); + } + return "Unknown"; +} + +function mapCpuUtilization(utilization: z.infer): number { + const userLoad = parseNumericValue(utilization.cpu?.user_load) ?? 0; + const systemLoad = parseNumericValue(utilization.cpu?.system_load) ?? 0; + const totalLoad = userLoad + systemLoad; + return Math.min(totalLoad, 100); +} + +function mapMemoryBytes(utilization: z.infer) { + const memory = utilization.memory; + const totalRealKib = parseNumericValue(memory?.total_real); + const realUsagePercent = parseNumericValue(memory?.real_usage); + + // DSM utilization API reports memory sizes in KiB, not bytes. + if (totalRealKib !== null && realUsagePercent !== null) { + const totalBytes = totalRealKib * 1024; + const usedBytes = Math.round((totalBytes * realUsagePercent) / 100); + return { + usedBytes, + availableBytes: Math.max(totalBytes - usedBytes, 0), + }; + } + + const availableKib = parseNumericValue(memory?.avail_real) ?? 0; + const cachedKib = parseNumericValue(memory?.cached) ?? 0; + const totalKib = totalRealKib ?? availableKib + cachedKib; + const usedKib = Math.max(totalKib - availableKib, 0); + + return { + usedBytes: usedKib * 1024, + availableBytes: availableKib * 1024, + }; +} + +function mapNetworkTraffic(utilization: z.infer) { + const totalNetwork = utilization.network?.find((entry) => entry.device === "total"); + if (!totalNetwork) { + return null; + } + + const received = parseNumericValue(totalNetwork.rx); + const transmitted = parseNumericValue(totalNetwork.tx); + if (received === null || transmitted === null) { + return null; + } + + return { + up: transmitted, + down: received, + }; +} + +function mapLoadAverage(utilization: z.infer) { + const oneMinute = parseNumericValue(utilization.cpu?.["1min_load"]); + const fiveMinute = parseNumericValue(utilization.cpu?.["5min_load"]); + const fifteenMinute = parseNumericValue(utilization.cpu?.["15min_load"]); + + if (oneMinute === null && fiveMinute === null && fifteenMinute === null) { + return null; + } + + return { + "1min": oneMinute ?? 0, + "5min": fiveMinute ?? 0, + "15min": fifteenMinute ?? 0, + }; +} + +function mapUptimeSeconds(systemInfo: z.infer): number { + if (typeof systemInfo.uptime === "number") { + return systemInfo.uptime; + } + + if (!systemInfo.up_time) { + return 0; + } + + const parts = systemInfo.up_time.split(":").map((part) => Number.parseInt(part, 10)); + if (parts.length !== 3 || parts.some((part) => Number.isNaN(part))) { + return 0; + } + + const [hours = 0, minutes = 0, seconds = 0] = parts; + return hours * 3600 + minutes * 60 + seconds; +} + +function mapCpuTemperature(systemInfo: z.infer): number | undefined { + const temperature = parseNumericValue(systemInfo.sys_temp ?? systemInfo.temperature); + return temperature === null ? undefined : temperature; +} + +function mapFileSystemEntries(volumes: SynologyVolumeRecord[]) { + return volumes.map((volume) => { + const availableBytes = Math.max(volume.totalBytes - volume.usedBytes, 0); + return { + deviceName: volume.name, + used: `${volume.usedBytes}`, + available: `${availableBytes}`, + percentage: (volume.usedBytes / volume.totalBytes) * 100, + }; + }); +} + +function mapVolumeSmartEntries(volumes: SynologyVolumeRecord[], diskRecords: SynologyDiskRecord[]) { + return volumes.map((volume) => { + const matchingDisks = diskRecords.filter((disk) => diskBelongsToVolume(disk, volume.name)); + const temperatures = matchingDisks + .map((disk) => disk.temperature) + .filter((temperature): temperature is number => temperature !== null); + const overallStatus = volume.status ?? pickWorstDiskStatus(matchingDisks); + + return { + deviceName: volume.name, + healthy: isHealthyStatus(overallStatus), + overallStatus, + temperature: temperatures.length > 0 ? Math.max(...temperatures) : null, + }; + }); +} + +function mergeDiskRecords( + loadInfoDisks: SynologyDiskRecord[], + smartInfoDisks: SynologyDiskRecord[], +): SynologyDiskRecord[] { + const diskMap = new Map(); + + for (const disk of loadInfoDisks) { + diskMap.set(disk.identifier, disk); + } + + for (const disk of smartInfoDisks) { + const existingDisk = diskMap.get(disk.identifier); + diskMap.set(disk.identifier, { + identifier: disk.identifier, + name: existingDisk?.name ?? disk.name, + status: disk.status ?? existingDisk?.status, + temperature: disk.temperature ?? existingDisk?.temperature ?? null, + volumeName: existingDisk?.volumeName ?? disk.volumeName, + }); + } + + return [...diskMap.values()]; +} + +function diskBelongsToVolume(disk: SynologyDiskRecord, volumeName: string): boolean { + if (!disk.volumeName) { + return false; + } + + return ( + disk.volumeName === volumeName || + disk.volumeName.endsWith(volumeName) || + volumeName.endsWith(disk.volumeName) || + disk.volumeName.includes(volumeName) || + volumeName.includes(disk.volumeName) + ); +} + +const UNHEALTHY_STATUS_TOKENS = ["critical", "error", "crash", "failed", "fail", "degraded", "warning"] as const; + +function getStatusSeverityIndex(status: string): number { + const normalizedStatus = status.toLowerCase(); + const index = UNHEALTHY_STATUS_TOKENS.findIndex((token) => normalizedStatus.includes(token)); + return index === -1 ? UNHEALTHY_STATUS_TOKENS.length : index; +} + +function pickWorstDiskStatus(disks: SynologyDiskRecord[]): string { + const statuses = disks + .map((disk) => disk.status) + .filter((status): status is string => status !== undefined && status.length > 0); + + if (statuses.length === 0) { + return "normal"; + } + + return statuses.reduce((worstStatus, currentStatus) => + getStatusSeverityIndex(currentStatus) < getStatusSeverityIndex(worstStatus) ? currentStatus : worstStatus, + ); +} + +function isHealthyStatus(status: string | undefined): boolean { + if (!status) { + return true; + } + + const normalizedStatus = status.toLowerCase(); + return !["crash", "degraded", "fail", "failed", "warning", "error", "critical"].some((unhealthyToken) => + normalizedStatus.includes(unhealthyToken), + ); +} + +function parseNumericValue(value: number | string | undefined): number | null { + if (value === undefined) { + return null; + } + const parsed = typeof value === "number" ? value : Number.parseFloat(value); + return Number.isFinite(parsed) ? parsed : null; +} diff --git a/packages/integrations/src/synology/synology-types.ts b/packages/integrations/src/synology/synology-types.ts new file mode 100644 index 000000000..c1f73b52e --- /dev/null +++ b/packages/integrations/src/synology/synology-types.ts @@ -0,0 +1,191 @@ +import { z } from "zod"; + +export const REQUEST_TIMEOUT_MS = 5000; + +export const AUTH_API_NAME = "SYNO.API.Auth"; +export const INFO_API_NAME = "SYNO.API.Info"; + +export const SESSION_ERROR_CODES = new Set([106, 107, 119]); + +export const synologyEnvelopeSchema = z.object({ + success: z.boolean(), + data: z.unknown().optional(), + error: z + .object({ + code: z.number(), + }) + .optional(), +}); + +export const synologyApiDefinitionSchema = z.object({ + path: z.string(), + maxVersion: z.number(), +}); + +export const synologyApiInfoResponseSchema = synologyEnvelopeSchema.extend({ + data: z.record(z.string(), synologyApiDefinitionSchema).optional(), +}); + +export const synologyAuthResponseSchema = synologyEnvelopeSchema.extend({ + data: z + .object({ + sid: z.string().optional(), + }) + .optional(), +}); + +const numericString = z.union([z.number(), z.string()]); + +export const synologySystemInfoDataSchema = z + .object({ + up_time: z.string().optional(), + uptime: z.number().optional(), + version_string: z.string().optional(), + firmware_ver: z.string().optional(), + version: z.union([z.string(), z.number()]).optional(), + model: z.string().optional(), + sys_temp: numericString.optional(), + temperature: numericString.optional(), + }) + .passthrough(); + +export const synologyVolumeInfoSchema = z.object({ + name: z.string(), + used_size: numericString.optional(), + total_size: numericString.optional(), + status: z.string().optional(), + vol_path: z.string().optional(), +}); + +export const synologyLegacyStorageDataSchema = z.object({ + vol_info: z.array(synologyVolumeInfoSchema).optional(), +}); + +export const synologyStorageV2VolumeSchema = z + .object({ + id: z.string().optional(), + vol_path: z.string().optional(), + display_name: z.string().optional(), + size: z + .object({ + total: z.union([z.number(), z.string()]).optional(), + used: z.union([z.number(), z.string()]).optional(), + }) + .optional(), + status: z.string().optional(), + }) + .passthrough(); + +export const synologyStorageV2DataSchema = z.object({ + volumes: z.array(synologyStorageV2VolumeSchema).optional(), +}); + +export const synologyUtilizationCpuSchema = z.object({ + user_load: numericString.optional(), + system_load: numericString.optional(), + other_load: numericString.optional(), + "1min_load": numericString.optional(), + "5min_load": numericString.optional(), + "15min_load": numericString.optional(), +}); + +export const synologyUtilizationMemorySchema = z.object({ + real_usage: numericString.optional(), + avail_real: numericString.optional(), + cached: numericString.optional(), + total_real: numericString.optional(), +}); + +export const synologyUtilizationNetworkSchema = z.object({ + device: z.string(), + rx: numericString.optional(), + tx: numericString.optional(), +}); + +export const synologyUtilizationDataSchema = z.object({ + cpu: synologyUtilizationCpuSchema.optional(), + memory: synologyUtilizationMemorySchema.optional(), + network: z.array(synologyUtilizationNetworkSchema).optional(), +}); + +export const synologyStorageDiskSchema = z + .object({ + id: z.string().optional(), + name: z.string().optional(), + display_name: z.string().optional(), + status: z.string().optional(), + temp: numericString.optional(), + temperature: numericString.optional(), + smart_status: z.string().optional(), + volume_id: z.string().optional(), + vol_path: z.string().optional(), + }) + .passthrough(); + +export const synologyStorageLoadInfoDataSchema = z + .object({ + disks: z.array(synologyStorageDiskSchema).optional(), + volumes: z.array(synologyStorageV2VolumeSchema).optional(), + overview_data: z + .object({ + volumes: z.array(synologyVolumeInfoSchema).optional(), + }) + .optional(), + }) + .passthrough(); + +export const synologySmartDiskSchema = z + .object({ + id: z.string().optional(), + disk_id: z.string().optional(), + name: z.string().optional(), + status: z.string().optional(), + temp: numericString.optional(), + temperature: numericString.optional(), + overall_status: z.string().optional(), + }) + .passthrough(); + +export const synologySmartInfoDataSchema = z + .object({ + disks: z.array(synologySmartDiskSchema).optional(), + items: z.array(synologySmartDiskSchema).optional(), + }) + .passthrough(); + +export const synologySystemStatusDataSchema = z + .object({ + reboot_required: z.boolean().optional(), + system_need_repair: z.boolean().optional(), + }) + .passthrough(); + +export const synologyUpgradeStatusDataSchema = z + .object({ + reboot_required: z.boolean().optional(), + upgrade_ready: z.boolean().optional(), + }) + .passthrough(); + +export const synologyUpgradeCheckDataSchema = z + .object({ + available: z.boolean().optional(), + update_count: z.number().optional(), + }) + .passthrough(); + +export type SynologyVolumeRecord = { + name: string; + displayName?: string; + usedBytes: number; + totalBytes: number; + status: string | undefined; +}; + +export type SynologyDiskRecord = { + identifier: string; + name: string; + status: string | undefined; + temperature: number | null; + volumeName: string | undefined; +}; diff --git a/packages/integrations/src/synology/test/synology-integration.spec.ts b/packages/integrations/src/synology/test/synology-integration.spec.ts new file mode 100644 index 000000000..e8a13f015 --- /dev/null +++ b/packages/integrations/src/synology/test/synology-integration.spec.ts @@ -0,0 +1,587 @@ +// @vitest-environment node + +import { Response } from "undici"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import type { z } from "zod"; + +vi.hoisted(() => { + process.env.CI = "true"; + process.env.NODE_ENV = "test"; + process.env.SECRET_ENCRYPTION_KEY = "0".repeat(64); +}); + +vi.mock("@homarr/core/infrastructure/http", () => ({ + fetchWithTrustedCertificatesAsync: vi.fn(), +})); + +vi.mock("@homarr/core/infrastructure/logs", () => ({ + createLogger: () => ({ + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }), +})); + +vi.mock("../../base/session-store", () => ({ + createSessionStore: () => ({ + getAsync: vi.fn().mockResolvedValue(null), + setAsync: vi.fn().mockResolvedValue(undefined), + clearAsync: vi.fn().mockResolvedValue(undefined), + }), +})); + +import { ResponseError } from "@homarr/common/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; + +import { IntegrationResponseError } from "../../base/errors/http/integration-response-error"; + +import type { IntegrationSecret } from "../../base/types"; +import { SynologyIntegration } from "../synology-integration"; +import type { synologyStorageV2DataSchema, synologyUtilizationDataSchema } from "../synology-types"; + +const TEST_URL = "https://synology.example.com:5001"; +const mockFetch = vi.mocked(fetchWithTrustedCertificatesAsync); + +const createIntegration = (secrets: IntegrationSecret[] = []) => + new SynologyIntegration({ + id: "test-synology", + name: "Test Synology", + url: TEST_URL, + externalUrl: null, + decryptedSecrets: secrets.length + ? secrets + : [ + { kind: "username", value: "homarr" }, + { kind: "password", value: "secret-password" }, + ], + }); + +const apiDefinitions = { + "SYNO.API.Auth": { path: "auth.cgi", maxVersion: 7 }, + "SYNO.API.Info": { path: "query.cgi", maxVersion: 1 }, + "SYNO.Core.System": { path: "entry.cgi", maxVersion: 3 }, + "SYNO.Core.System.Utilization": { path: "entry.cgi", maxVersion: 1 }, + "SYNO.Storage.CGI.Storage": { path: "entry.cgi", maxVersion: 1 }, + "SYNO.Storage.CGI.Smart": { path: "entry.cgi", maxVersion: 1 }, + "SYNO.Core.System.Status": { path: "entry.cgi", maxVersion: 1 }, + "SYNO.Core.Upgrade": { path: "entry.cgi", maxVersion: 1 }, + "SYNO.Core.Upgrade.Server": { path: "entry.cgi", maxVersion: 1 }, +}; + +const systemInfoResponse = { + success: true as const, + data: { + up_time: "48:30:15", + version_string: "DSM 7.3-12345", + model: "DS920+", + sys_temp: 42, + }, +}; + +const utilizationResponse = { + success: true, + data: { + cpu: { + user_load: 12, + system_load: 8, + "1min_load": 10, + "5min_load": 14, + "15min_load": 9, + }, + memory: { + real_usage: 35, + total_real: 8 * 1024 * 1024, + avail_real: Math.round(8 * 1024 * 1024 * 0.65), + cached: 900_000, + }, + network: [ + { device: "total", rx: 4096, tx: 8192 }, + { device: "eth0", rx: 4096, tx: 8192 }, + ], + }, +}; + +const storageV2Response = { + success: true, + data: { + volumes: [ + { + id: "volume_1", + display_name: "Data Volume 1", + size: { total: 10_000_000_000, used: 4_000_000_000 }, + status: "normal", + }, + { + id: "volume_2", + display_name: "Data Volume 2", + size: { total: 20_000_000_000, used: 5_000_000_000 }, + status: "degraded", + }, + ], + }, +}; + +const storageLoadInfoResponse = { + success: true, + data: { + disks: [ + { + id: "sda", + display_name: "Drive 1", + temp: 38, + status: "normal", + volume_id: "volume_1", + }, + { + id: "sdb", + display_name: "Drive 2", + temp: 44, + status: "normal", + volume_id: "volume_2", + }, + ], + }, +}; + +const smartInfoResponse = { + success: true, + data: { + disks: [ + { + disk_id: "sda", + name: "Drive 1", + overall_status: "normal", + temp: 39, + }, + ], + }, +}; + +const systemStatusResponse = { + success: true, + data: { + reboot_required: true, + }, +}; + +const upgradeStatusResponse = { + success: true, + data: { + reboot_required: false, + }, +}; + +const upgradeCheckResponse = { + success: true, + data: { + available: true, + update_count: 2, + }, +}; + +const toUrlString = (url: unknown): string => String(url); + +const getApiFromUrl = (url: string) => { + const parsedUrl = new URL(url); + return parsedUrl.searchParams.get("api"); +}; + +const getMethodFromUrl = (url: string) => { + const parsedUrl = new URL(url); + return parsedUrl.searchParams.get("method"); +}; + +const getTypeFromUrl = (url: string) => { + const parsedUrl = new URL(url); + return parsedUrl.searchParams.get("type"); +}; + +const mockSuccessfulFlow = (options?: { + skipStorageV2?: boolean; + skipOptional?: boolean; + systemInfo?: { + success: boolean; + data: Record; + }; + utilization?: { success: boolean; data: z.input }; + storageV2?: { success: boolean; data: z.input }; + storageLoadInfo?: typeof storageLoadInfoResponse; +}) => { + mockFetch.mockImplementation((url) => { + const urlString = toUrlString(url); + const apiName = getApiFromUrl(urlString); + const method = getMethodFromUrl(urlString); + const type = getTypeFromUrl(urlString); + + if (apiName === "SYNO.API.Info" && method === "query") { + const query = new URL(urlString).searchParams.get("query") ?? "SYNO.API.Auth"; + const definition = apiDefinitions[query as keyof typeof apiDefinitions]; + return Promise.resolve( + new Response(JSON.stringify({ success: true, data: { [query]: definition } }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ) as unknown as ReturnType; + } + + if (apiName === "SYNO.API.Auth" && method === "login") { + return Promise.resolve( + new Response(JSON.stringify({ success: true, data: { sid: "test-session-id" } }), { + status: 200, + headers: { + "Content-Type": "application/json", + "Set-Cookie": "id=test-session-id; Path=/; HttpOnly", + }, + }), + ) as unknown as ReturnType; + } + + if (apiName === "SYNO.Core.System" && method === "info" && type === "storage_v2") { + if (options?.skipStorageV2) { + return Promise.resolve( + new Response(JSON.stringify({ success: false, error: { code: 103 } }), { status: 200 }), + ) as unknown as ReturnType; + } + return Promise.resolve( + new Response(JSON.stringify(options?.storageV2 ?? storageV2Response), { status: 200 }), + ) as unknown as ReturnType; + } + + if (apiName === "SYNO.Core.System" && method === "info" && type === "storage") { + return Promise.resolve( + new Response( + JSON.stringify({ + success: true, + data: { + vol_info: [ + { + name: "volume_1", + used_size: 4_000_000_000, + total_size: 10_000_000_000, + status: "normal", + }, + ], + }, + }), + { status: 200 }, + ), + ) as unknown as ReturnType; + } + + if (apiName === "SYNO.Core.System" && method === "info" && !type) { + return Promise.resolve( + new Response(JSON.stringify(options?.systemInfo ?? systemInfoResponse), { status: 200 }), + ) as unknown as ReturnType; + } + + if (apiName === "SYNO.Core.System.Utilization" && method === "get") { + return Promise.resolve( + new Response(JSON.stringify(options?.utilization ?? utilizationResponse), { status: 200 }), + ) as unknown as ReturnType; + } + + if (options?.skipOptional) { + return Promise.resolve( + new Response(JSON.stringify({ success: false, error: { code: 105 } }), { status: 200 }), + ) as unknown as ReturnType; + } + + if (apiName === "SYNO.Storage.CGI.Storage" && method === "load_info") { + return Promise.resolve( + new Response(JSON.stringify(options?.storageLoadInfo ?? storageLoadInfoResponse), { status: 200 }), + ) as unknown as ReturnType; + } + + if (apiName === "SYNO.Storage.CGI.Smart" && method === "get_smart_info") { + return Promise.resolve(new Response(JSON.stringify(smartInfoResponse), { status: 200 })) as unknown as ReturnType< + typeof fetchWithTrustedCertificatesAsync + >; + } + + if (apiName === "SYNO.Core.System.Status" && method === "get") { + return Promise.resolve( + new Response(JSON.stringify(systemStatusResponse), { status: 200 }), + ) as unknown as ReturnType; + } + + if (apiName === "SYNO.Core.Upgrade" && method === "status") { + return Promise.resolve( + new Response(JSON.stringify(upgradeStatusResponse), { status: 200 }), + ) as unknown as ReturnType; + } + + if (apiName === "SYNO.Core.Upgrade.Server" && method === "check") { + return Promise.resolve( + new Response(JSON.stringify(upgradeCheckResponse), { status: 200 }), + ) as unknown as ReturnType; + } + + return Promise.resolve( + new Response(JSON.stringify({ success: false, error: { code: 102 } }), { status: 200 }), + ) as unknown as ReturnType; + }); +}; + +beforeEach(() => { + mockFetch.mockReset(); +}); + +describe("SynologyIntegration.getSystemInfoAsync", () => { + test("maps all supported health monitoring fields", async () => { + mockSuccessfulFlow(); + const integration = createIntegration(); + const result = await integration.getSystemInfoAsync(); + + expect(result.version).toBe("DSM 7.3-12345"); + expect(result.cpuModelName).toBe("DS920+"); + expect(result.cpuUtilization).toBe(20); + expect(result.memUsedInBytes).toBe(Math.round((8 * 1024 * 1024 * 1024 * 35) / 100)); + expect(result.memAvailableInBytes).toBe(8 * 1024 * 1024 * 1024 - Math.round((8 * 1024 * 1024 * 1024 * 35) / 100)); + expect(result.uptime).toBe(48 * 3600 + 30 * 60 + 15); + expect(result.cpuTemp).toBe(42); + expect(result.network).toEqual({ up: 8192, down: 4096 }); + expect(result.loadAverage).toEqual({ "1min": 10, "5min": 14, "15min": 9 }); + expect(result.rebootRequired).toBe(true); + expect(result.availablePkgUpdates).toBe(2); + expect(result.gpu).toEqual([]); + expect(result.fileSystem).toHaveLength(2); + expect(result.fileSystem[0]).toEqual({ + deviceName: "volume_1", + used: "4000000000", + available: "6000000000", + percentage: 40, + }); + expect(result.smart).toEqual([ + { + deviceName: "volume_1", + healthy: true, + overallStatus: "normal", + temperature: 39, + }, + { + deviceName: "volume_2", + healthy: false, + overallStatus: "degraded", + temperature: 44, + }, + ]); + }); + + test("picks worst disk status when volume status is missing", async () => { + mockSuccessfulFlow({ + storageV2: { + success: true, + data: { + volumes: [ + { + id: "volume_1", + display_name: "Data Volume 1", + size: { total: 10_000_000_000, used: 4_000_000_000 }, + }, + ], + }, + }, + storageLoadInfo: { + success: true, + data: { + disks: [ + { + id: "sda", + display_name: "Drive 1", + temp: 38, + status: "normal", + volume_id: "volume_1", + }, + { + id: "sdb", + display_name: "Drive 2", + temp: 40, + status: "degraded", + volume_id: "volume_1", + }, + ], + }, + }, + }); + const integration = createIntegration(); + const result = await integration.getSystemInfoAsync(); + + expect(result.smart).toEqual([ + { + deviceName: "volume_1", + healthy: false, + overallStatus: "degraded", + temperature: 40, + }, + ]); + }); + + test("falls back to legacy storage info when storage_v2 is unavailable", async () => { + mockSuccessfulFlow({ skipStorageV2: true }); + const integration = createIntegration(); + const result = await integration.getSystemInfoAsync(); + + expect(result.fileSystem).toHaveLength(1); + expect(result.fileSystem[0]?.deviceName).toBe("volume_1"); + }); + + test("continues when optional tier-2 calls fail", async () => { + mockSuccessfulFlow({ skipOptional: true }); + const integration = createIntegration(); + const result = await integration.getSystemInfoAsync(); + + expect(result.cpuUtilization).toBe(20); + expect(result.rebootRequired).toBe(false); + expect(result.availablePkgUpdates).toBe(0); + expect(result.smart.every((entry) => entry.temperature === null)).toBe(true); + }); + + test("converts memory values from KiB to bytes", async () => { + mockSuccessfulFlow({ + utilization: { + ...utilizationResponse, + data: { + ...utilizationResponse.data, + memory: { + real_usage: 17, + total_real: 16_351_532, + avail_real: 426_568, + }, + }, + }, + }); + const integration = createIntegration(); + const result = await integration.getSystemInfoAsync(); + + const totalBytes = 16_351_532 * 1024; + expect(result.memUsedInBytes).toBe(Math.round((totalBytes * 17) / 100)); + expect(result.memAvailableInBytes).toBe(totalBytes - Math.round((totalBytes * 17) / 100)); + }); + + test("caps cpu utilization at 100 when user and system load exceed 100", async () => { + mockSuccessfulFlow({ + utilization: { + ...utilizationResponse, + data: { + ...utilizationResponse.data, + cpu: { + ...utilizationResponse.data.cpu, + user_load: 80, + system_load: 30, + }, + }, + }, + }); + const integration = createIntegration(); + const result = await integration.getSystemInfoAsync(); + + expect(result.cpuUtilization).toBe(100); + }); + + test("returns undefined cpu temperature when sensor is absent", async () => { + mockSuccessfulFlow({ + systemInfo: { + success: true, + data: { + up_time: "01:00:00", + version_string: "DSM 7.3-12345", + model: "DS920+", + }, + }, + }); + const integration = createIntegration(); + const result = await integration.getSystemInfoAsync(); + expect(result.cpuTemp).toBeUndefined(); + }); +}); + +describe("SynologyIntegration.listStorageVolumesAsync", () => { + test("returns auto-detected volume names", async () => { + mockSuccessfulFlow(); + const integration = createIntegration(); + const volumes = await integration.listStorageVolumesAsync(); + + expect(volumes).toEqual([ + { value: "test-synology:volume_1", label: "Data Volume 1 (Test Synology)" }, + { value: "test-synology:volume_2", label: "Data Volume 2 (Test Synology)" }, + ]); + }); +}); + +describe("SynologyIntegration authentication", () => { + test("rejects two-factor authentication during login", async () => { + mockFetch.mockImplementation((url) => { + const urlString = toUrlString(url); + const apiName = getApiFromUrl(urlString); + const method = getMethodFromUrl(urlString); + + if (apiName === "SYNO.API.Info" && method === "query") { + const query = new URL(urlString).searchParams.get("query") ?? "SYNO.API.Auth"; + const definition = apiDefinitions[query as keyof typeof apiDefinitions]; + return Promise.resolve( + new Response(JSON.stringify({ success: true, data: { [query]: definition } }), { status: 200 }), + ) as unknown as ReturnType; + } + + if (apiName === "SYNO.API.Auth" && method === "login") { + return Promise.resolve( + new Response(JSON.stringify({ success: false, error: { code: 403 } }), { status: 200 }), + ) as unknown as ReturnType; + } + + return Promise.resolve(new Response("{}", { status: 500 })) as unknown as ReturnType< + typeof fetchWithTrustedCertificatesAsync + >; + }); + + const integration = createIntegration(); + await expect(integration.getSystemInfoAsync()).rejects.toThrow(); + }); + + test("does not leak credentials in login error URLs", async () => { + mockFetch.mockImplementation((url, init) => { + const urlString = toUrlString(url); + const apiName = getApiFromUrl(urlString); + const method = getMethodFromUrl(urlString); + + if (apiName === "SYNO.API.Info" && method === "query") { + const query = new URL(urlString).searchParams.get("query") ?? "SYNO.API.Auth"; + const definition = apiDefinitions[query as keyof typeof apiDefinitions]; + return Promise.resolve( + new Response(JSON.stringify({ success: true, data: { [query]: definition } }), { status: 200 }), + ) as unknown as ReturnType; + } + + if (apiName === "SYNO.API.Auth" && method === "login") { + expect(init?.method).toBe("POST"); + expect(init?.body).toContain("account=homarr"); + expect(init?.body).toContain("passwd=secret-password"); + expect(urlString).not.toContain("account="); + expect(urlString).not.toContain("passwd="); + + return Promise.resolve( + new Response(JSON.stringify({ success: false, error: { code: 403 } }), { status: 200 }), + ) as unknown as ReturnType; + } + + return Promise.resolve(new Response("{}", { status: 500 })) as unknown as ReturnType< + typeof fetchWithTrustedCertificatesAsync + >; + }); + + const integration = createIntegration(); + + try { + await integration.getSystemInfoAsync(); + expect.unreachable("Expected login to fail"); + } catch (error) { + expect(error).toBeInstanceOf(IntegrationResponseError); + const responseError = (error as IntegrationResponseError).cause; + expect(responseError).toBeInstanceOf(ResponseError); + expect(responseError.url).not.toContain("secret-password"); + expect(responseError.url).not.toContain("passwd="); + expect(responseError.url).not.toContain("account=homarr"); + } + }); +}); diff --git a/packages/integrations/src/traefik/traefik-integration.ts b/packages/integrations/src/traefik/traefik-integration.ts new file mode 100644 index 000000000..0dc8cf57f --- /dev/null +++ b/packages/integrations/src/traefik/traefik-integration.ts @@ -0,0 +1,163 @@ +import { ResponseError } from "@homarr/common/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; + +import type { IntegrationTestingInput } from "../base/integration"; +import { Integration } from "../base/integration"; +import type { TestingResult } from "../base/test-connection/test-connection-service"; +import type { + TraefikDashboardData, + TraefikProtocolSummary, + TraefikResource, + TraefikResourceStatus, + TraefikResourceSummary, +} from "./traefik-types"; +import { traefikResourcesSchema, traefikVersionSchema } from "./traefik-types"; + +type ResourcePath = + | "/api/http/routers" + | "/api/http/services" + | "/api/http/middlewares" + | "/api/tcp/routers" + | "/api/tcp/services" + | "/api/tcp/middlewares" + | "/api/udp/routers" + | "/api/udp/services" + | "/api/entrypoints"; + +export class TraefikIntegration extends Integration { + protected async testingAsync(input: IntegrationTestingInput): Promise { + const response = await input.fetchAsync(this.url("/api/version"), { headers: this.getAuthHeaders() }); + + if (!response.ok) { + throw new ResponseError(response); + } + + traefikVersionSchema.parse(await response.json()); + return { success: true }; + } + + public async getDashboardDataAsync(): Promise { + const [ + versionResult, + entryPointsResult, + httpRoutersResult, + httpServicesResult, + httpMiddlewaresResult, + tcpRoutersResult, + tcpServicesResult, + tcpMiddlewaresResult, + udpRoutersResult, + udpServicesResult, + ] = await Promise.allSettled([ + this.getVersionAsync(), + this.getResourcesAsync("/api/entrypoints"), + this.getResourcesAsync("/api/http/routers"), + this.getResourcesAsync("/api/http/services"), + this.getResourcesAsync("/api/http/middlewares"), + this.getResourcesAsync("/api/tcp/routers"), + this.getResourcesAsync("/api/tcp/services"), + this.getResourcesAsync("/api/tcp/middlewares"), + this.getResourcesAsync("/api/udp/routers"), + this.getResourcesAsync("/api/udp/services"), + ]); + const version = versionResult.status === "fulfilled" ? versionResult.value : null; + const entryPoints = entryPointsResult.status === "fulfilled" ? entryPointsResult.value : []; + const httpRouters = httpRoutersResult.status === "fulfilled" ? httpRoutersResult.value : []; + const httpServices = httpServicesResult.status === "fulfilled" ? httpServicesResult.value : []; + const httpMiddlewares = httpMiddlewaresResult.status === "fulfilled" ? httpMiddlewaresResult.value : []; + const tcpRouters = tcpRoutersResult.status === "fulfilled" ? tcpRoutersResult.value : []; + const tcpServices = tcpServicesResult.status === "fulfilled" ? tcpServicesResult.value : []; + const tcpMiddlewares = tcpMiddlewaresResult.status === "fulfilled" ? tcpMiddlewaresResult.value : []; + const udpRouters = udpRoutersResult.status === "fulfilled" ? udpRoutersResult.value : []; + const udpServices = udpServicesResult.status === "fulfilled" ? udpServicesResult.value : []; + + return { + version, + entryPoints: entryPoints.map((entryPoint, index) => entryPoint.name ?? `entrypoint-${index + 1}`), + http: this.createProtocolSummary(httpRouters, httpServices, httpMiddlewares), + tcp: this.createProtocolSummary(tcpRouters, tcpServices, tcpMiddlewares), + udp: { + routers: this.summarizeResources(udpRouters), + services: this.summarizeResources(udpServices), + }, + }; + } + + private async getVersionAsync() { + const response = await this.fetchOkAsync("/api/version"); + const version = traefikVersionSchema.parse(await response.json()); + return version.Version ?? null; + } + + private async getResourcesAsync(path: ResourcePath) { + const response = await this.fetchOkAsync(path); + return traefikResourcesSchema.parse(await response.json()); + } + + private createProtocolSummary( + routers: TraefikResource[], + services: TraefikResource[], + middlewares: TraefikResource[], + ): TraefikProtocolSummary { + return { + routers: this.summarizeResources(routers), + services: this.summarizeResources(services), + middlewares: this.summarizeResources(middlewares), + }; + } + + private summarizeResources(resources: TraefikResource[]): TraefikResourceSummary { + return resources.reduce( + (summary, resource) => { + const status = this.normalizeStatus(resource.status); + summary.total += 1; + + if (status === "enabled") summary.enabled += 1; + if (status === "warning") summary.warnings += 1; + if (status === "error") summary.errors += 1; + + return summary; + }, + { total: 0, enabled: 0, warnings: 0, errors: 0 }, + ); + } + + private normalizeStatus(status: string | undefined): TraefikResourceStatus { + if (!status) return "unknown"; + const normalized = status.toLowerCase(); + + if (normalized === "enabled" || normalized === "disabled" || normalized === "warning" || normalized === "error") { + return normalized; + } + + return "unknown"; + } + + private async fetchOkAsync(path: ResourcePath | "/api/version") { + const response = await fetchWithTrustedCertificatesAsync(this.url(path), { + headers: this.getAuthHeaders(), + timeout: 10_000, + }); + + if (!response.ok) { + throw new ResponseError(response); + } + + return response; + } + + private getAuthHeaders(): Record { + if (this.hasSecretValue("username") && this.hasSecretValue("password")) { + const credentials = Buffer.from(`${this.getSecretValue("username")}:${this.getSecretValue("password")}`).toString( + "base64", + ); + return { Authorization: `Basic ${credentials}` }; + } + + if (this.hasSecretValue("apiKey")) { + return { Authorization: `Bearer ${this.getSecretValue("apiKey")}` }; + } + + return {}; + } +} diff --git a/packages/integrations/src/traefik/traefik-types.ts b/packages/integrations/src/traefik/traefik-types.ts new file mode 100644 index 000000000..5874ef41b --- /dev/null +++ b/packages/integrations/src/traefik/traefik-types.ts @@ -0,0 +1,43 @@ +import { z } from "zod"; + +export const traefikResourceStatusSchema = z.enum(["enabled", "disabled", "warning", "error", "unknown"]); + +const traefikResourceSchema = z + .object({ + name: z.string().optional(), + provider: z.string().optional(), + status: z.string().optional(), + }) + .passthrough(); + +export const traefikVersionSchema = z + .object({ + Version: z.string().optional(), + }) + .passthrough(); + +export const traefikResourcesSchema = z.array(traefikResourceSchema); + +export interface TraefikResourceSummary { + total: number; + enabled: number; + warnings: number; + errors: number; +} + +export interface TraefikProtocolSummary { + routers: TraefikResourceSummary; + services: TraefikResourceSummary; + middlewares: TraefikResourceSummary; +} + +export interface TraefikDashboardData { + version: string | null; + entryPoints: string[]; + http: TraefikProtocolSummary; + tcp: TraefikProtocolSummary; + udp: Omit; +} + +export type TraefikResource = z.infer[number]; +export type TraefikResourceStatus = z.infer; diff --git a/packages/integrations/src/types.ts b/packages/integrations/src/types.ts index 5a78382c8..e4d1ccb8b 100644 --- a/packages/integrations/src/types.ts +++ b/packages/integrations/src/types.ts @@ -1,4 +1,6 @@ export * from "./interfaces/calendar/calendar-types"; +export * from "./interfaces/media-organizer/media-organizer-types"; +export type { IMediaOrganizerIntegration } from "./interfaces/media-organizer/media-organizer-integration"; export * from "./interfaces/dns-hole-summary/dns-hole-summary-types"; export * from "./interfaces/network-controller-summary/network-controller-summary-types"; export * from "./interfaces/firewall-summary/firewall-summary-types"; @@ -15,7 +17,6 @@ export * from "./opnsense/opnsense-types"; export * from "./interfaces/media-releases"; export * from "./coolify/coolify-types"; export * from "./anchor/anchor-types"; -export * from "./interfaces/timetable/timetable-types"; export * from "./tracearr/tracearr-types"; export * from "./speedtest-tracker/speedtest-tracker-types"; export * from "./uptime-kuma/uptime-kuma-types"; @@ -26,3 +27,4 @@ export * from "./umami/umami-types"; export * from "./gluetun/gluetun-types"; export * from "./beszel/beszel-types"; export * from "./open-webui/open-webui-types"; +export * from "./traefik/traefik-types"; diff --git a/packages/integrations/test/bazarr-unit.spec.ts b/packages/integrations/test/bazarr-unit.spec.ts new file mode 100644 index 000000000..eed0f9082 --- /dev/null +++ b/packages/integrations/test/bazarr-unit.spec.ts @@ -0,0 +1,110 @@ +// @vitest-environment node + +import { Response } from "undici"; +import { beforeEach, describe, expect, test, vi } from "vitest"; + +import { createDb } from "@homarr/db/test"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; + +import type { IntegrationSecret } from "../src/base/types"; +import { BazarrIntegration } from "../src/bazarr/bazarr-integration"; + +vi.mock("@homarr/db", async (importActual) => { + const actual = await importActual(); + return { ...actual, db: createDb() }; +}); + +vi.mock("@homarr/core/infrastructure/certificates", async (importActual) => { + const actual = await importActual(); + return { + ...actual, + getTrustedCertificateHostnamesAsync: vi.fn().mockResolvedValue([]), + }; +}); + +vi.mock("@homarr/core/infrastructure/http", () => ({ + fetchWithTrustedCertificatesAsync: vi.fn(), +})); + +const TEST_URL = "https://bazarr.example.com"; +const API_KEY = "test-api-key"; + +const mockFetch = vi.mocked(fetchWithTrustedCertificatesAsync); + +const createIntegration = (decryptedSecrets: IntegrationSecret[] = [{ kind: "apiKey", value: API_KEY }]) => + new BazarrIntegration({ + id: "test-bazarr", + name: "Test Bazarr", + url: TEST_URL, + externalUrl: null, + decryptedSecrets, + }); + +beforeEach(() => { + mockFetch.mockReset(); +}); + +describe("BazarrIntegration getBadgesAsync", () => { + test("maps badge counts from the Bazarr API", async () => { + mockFetch.mockImplementation( + () => + Promise.resolve( + new Response( + JSON.stringify({ + episodes: 12, + movies: 4, + providers: 1, + status: 2, + sonarr_signalr: "LIVE", + radarr_signalr: "", + announcements: 0, + }), + { + status: 200, + headers: { "content-type": "application/json" }, + }, + ), + ) as unknown as ReturnType, + ); + + const badges = await createIntegration().getBadgesAsync(); + + expect(badges).toStrictEqual({ + episodes: 12, + movies: 4, + providers: 1, + status: 2, + sonarr_signalr: "LIVE", + radarr_signalr: "", + announcements: 0, + }); + }); + + test("sends the API key in the X-API-KEY header", async () => { + mockFetch.mockImplementation( + () => + Promise.resolve( + new Response( + JSON.stringify({ + episodes: 0, + movies: 0, + providers: 0, + status: 0, + sonarr_signalr: "", + radarr_signalr: "", + announcements: 0, + }), + { + status: 200, + headers: { "content-type": "application/json" }, + }, + ), + ) as unknown as ReturnType, + ); + + await createIntegration().getBadgesAsync(); + + const [, requestInit] = mockFetch.mock.calls[0] ?? []; + expect(requestInit?.headers).toMatchObject({ "X-API-KEY": API_KEY }); + }); +}); diff --git a/packages/integrations/test/glances-unit.spec.ts b/packages/integrations/test/glances-unit.spec.ts index 121ab594a..07f908e9e 100644 --- a/packages/integrations/test/glances-unit.spec.ts +++ b/packages/integrations/test/glances-unit.spec.ts @@ -1,11 +1,11 @@ // @vitest-environment node -import { describe, expect, test, vi } from "vitest"; +import { beforeEach, describe, expect, test, vi } from "vitest"; import { createDb } from "@homarr/db/test"; import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; -import { GlancesIntegration } from "../src/glances/glances-integration"; +import { GlancesIntegration, parseGlancesCpuTempFromSensors } from "../src/glances/glances-integration"; // ─── module mocks ──────────────────────────────────────────────────────────── @@ -64,6 +64,19 @@ function makeResponse(body: unknown) { } as never; } +function mockGlancesFetch(statsBody: unknown, sensorsBody: unknown) { + mockFetch.mockImplementation(((url: string | URL) => { + const urlString = url.toString(); + if (urlString.endsWith("/api/4/all")) { + return Promise.resolve(makeResponse(statsBody)); + } + if (urlString.endsWith("/api/4/sensors")) { + return Promise.resolve(makeResponse(sensorsBody)); + } + return Promise.reject(new Error(`Unexpected fetch URL: ${urlString}`)); + }) as typeof fetchWithTrustedCertificatesAsync); +} + const integrationInput = { id: "test-glances", name: "Glances", @@ -75,8 +88,12 @@ const integrationInput = { // ─── tests ─────────────────────────────────────────────────────────────────── describe("GlancesIntegration schema", () => { + beforeEach(() => { + mockFetch.mockReset(); + }); + test("parses stats when quicklook is present", async () => { - mockFetch.mockResolvedValueOnce(makeResponse({ ...baseStats, quicklook: { cpu_name: "Apple M1" } })); + mockGlancesFetch({ ...baseStats, quicklook: { cpu_name: "Apple M1" } }, []); const integration = new GlancesIntegration(integrationInput); const result = await integration.getSystemInfoAsync(); @@ -86,7 +103,7 @@ describe("GlancesIntegration schema", () => { }); test("returns Unknown cpuModelName when quicklook is absent (macOS scenario)", async () => { - mockFetch.mockResolvedValueOnce(makeResponse(baseStats)); + mockGlancesFetch(baseStats, []); const integration = new GlancesIntegration(integrationInput); const result = await integration.getSystemInfoAsync(); @@ -95,9 +112,41 @@ describe("GlancesIntegration schema", () => { }); test("does not throw a Zod parse error when quicklook is missing", async () => { - mockFetch.mockResolvedValueOnce(makeResponse(baseStats)); + mockGlancesFetch(baseStats, []); const integration = new GlancesIntegration(integrationInput); await expect(integration.getSystemInfoAsync()).resolves.not.toThrow(); }); + + test("reads CPU temperature from sensors", async () => { + mockGlancesFetch(baseStats, [ + { label: "CPU", unit: "C", value: 58, type: "temperature_core" }, + { label: "Core 0", unit: "C", value: 54, type: "temperature_core" }, + ]); + + const integration = new GlancesIntegration(integrationInput); + const result = await integration.getSystemInfoAsync(); + + expect(result.cpuTemp).toBe(58); + }); +}); + +describe("parseGlancesCpuTempFromSensors", () => { + test("prefers CPU label over core sensors", () => { + const result = parseGlancesCpuTempFromSensors([ + { label: "Core 0", value: 54, type: "temperature_core" }, + { label: "CPU", value: 58, type: "temperature_core" }, + ]); + + expect(result).toBe(58); + }); + + test("falls back to Package id 0", () => { + const result = parseGlancesCpuTempFromSensors([ + { label: "Core 0", value: 54, type: "temperature_core" }, + { label: "Package id 0", value: 62, type: "temperature_core" }, + ]); + + expect(result).toBe(62); + }); }); diff --git a/packages/integrations/test/lidarr-unit.spec.ts b/packages/integrations/test/lidarr-unit.spec.ts new file mode 100644 index 000000000..b788565a6 --- /dev/null +++ b/packages/integrations/test/lidarr-unit.spec.ts @@ -0,0 +1,76 @@ +// @vitest-environment node + +import { describe, expect, test, vi } from "vitest"; + +import { createDb } from "@homarr/db/test"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; + +import { LidarrIntegration } from "../src/media-organizer/lidarr/lidarr-integration"; + +vi.mock("@homarr/db", async (importActual) => { + const actual = await importActual(); + return { ...actual, db: createDb() }; +}); + +vi.mock("@homarr/core/infrastructure/certificates", async (importActual) => { + const actual = await importActual(); + return { + ...actual, + getTrustedCertificateHostnamesAsync: vi.fn().mockResolvedValue([]), + }; +}); + +vi.mock("@homarr/core/infrastructure/http", () => ({ + fetchWithTrustedCertificatesAsync: vi.fn(), +})); + +const mockFetch = vi.mocked(fetchWithTrustedCertificatesAsync); + +const integrationInput = { + id: "test-lidarr", + name: "Lidarr", + url: "http://localhost:8686", + decryptedSecrets: [{ kind: "apiKey" as const, value: "test-key" }], + externalUrl: null, +}; + +describe("LidarrIntegration calendar", () => { + test("includes Lidarr artist link in calendar events", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve([ + { + title: "Test Album", + overview: "Overview", + images: [], + artist: { + artistName: "Test Artist", + titleSlug: "test-artist", + links: [{ name: "last", url: "https://last.fm/music/Test+Artist" }], + }, + releaseDate: "2026-01-01T00:00:00Z", + }, + ]), + } as never); + + const integration = new LidarrIntegration(integrationInput); + const events = await integration.getCalendarEventsAsync(new Date("2026-01-01"), new Date("2026-01-31")); + + expect(events[0]?.links[0]).toMatchObject({ + name: "Lidarr", + href: "http://localhost:8686/artist/test-artist", + logo: "/images/apps/lidarr.svg", + }); + // The existing external artist link must survive the Lidarr-link prepend. + expect(events[0]?.links).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: "LastFM", + href: "https://last.fm/music/Test+Artist", + logo: "/images/apps/lastfm.svg", + }), + ]), + ); + }); +}); diff --git a/packages/old-import/src/user-schema.ts b/packages/old-import/src/user-schema.ts index 84e544ea3..1bbe91211 100644 --- a/packages/old-import/src/user-schema.ts +++ b/packages/old-import/src/user-schema.ts @@ -1,5 +1,7 @@ import { z } from "zod/v4"; +import { requiredNullableEmailSchema } from "@homarr/validation/email"; + const regexEncryptedSchema = z.string().regex(/^[a-f0-9]+\.[a-f0-9]+$/g); const encryptedSchema = z.custom<`${string}.${string}`>((value) => regexEncryptedSchema.safeParse(value).success); @@ -7,7 +9,7 @@ const encryptedSchema = z.custom<`${string}.${string}`>((value) => regexEncrypte export const oldmarrImportUserSchema = z.object({ id: z.string(), name: z.string(), - email: z.string().email().nullable(), + email: requiredNullableEmailSchema, emailVerified: z.date().nullable(), image: z.string().nullable(), isAdmin: z.boolean(), diff --git a/packages/old-import/src/widgets/options.ts b/packages/old-import/src/widgets/options.ts index b407d0e45..a4ae66bb0 100644 --- a/packages/old-import/src/widgets/options.ts +++ b/packages/old-import/src/widgets/options.ts @@ -165,6 +165,7 @@ const optionMapping: OptionMapping = { "sectionIndicatorColor" in oldOptions ? oldOptions.sectionIndicatorColor : undefined, showUptime: () => undefined, gpu: () => undefined, + visibleStorageVolumes: () => undefined, visibleClusterSections: (oldOptions) => { if (!("showNode" in oldOptions)) return undefined; diff --git a/packages/redis/src/index.ts b/packages/redis/src/index.ts index 6c99c11b6..02258a2e0 100644 --- a/packages/redis/src/index.ts +++ b/packages/redis/src/index.ts @@ -1,42 +1,22 @@ import type { LogLevel } from "@homarr/core/infrastructure/logs/constants"; -import { createListChannel, createQueueChannel, createSubPubChannel } from "./lib/channel"; +import { createListChannel, createSubPubChannel } from "./lib/channel"; export { - createCacheChannel, - createItemAndIntegrationChannel, - createItemChannel, - createIntegrationOptionsChannel, - createWidgetOptionsChannel, - createChannelWithLatestAndEvents, - createChannelEventHistory, handshakeAsync, createSubPubChannel, createGetSetChannel, - createQueryCacheChannel, - getAllQueryCacheAsync, + setQueryCacheAsync, + getQueryCacheAsync, + removeQueryCacheAsync, invalidateIntegrationCacheAsync, } from "./lib/channel"; -export { createIntegrationHistoryChannel } from "./lib/channels/history-channel"; - -export const exampleChannel = createSubPubChannel<{ message: string }>("example"); export const pingChannel = createSubPubChannel< { url: string; statusCode: number; durationMs: number } | { url: string; error: string } >("ping"); export const pingUrlChannel = createListChannel("ping-url"); -export const homeAssistantEntityState = createSubPubChannel<{ - entityId: string; - state: string; -}>("home-assistant/entity-state"); - -export const queueChannel = createQueueChannel<{ - name: string; - executionDate: Date; - data: unknown; -}>("common-queue"); - export interface LoggerMessage { message: string; level: LogLevel; diff --git a/packages/redis/src/lib/channel.ts b/packages/redis/src/lib/channel.ts index d04d1499a..1b1e582c1 100644 --- a/packages/redis/src/lib/channel.ts +++ b/packages/redis/src/lib/channel.ts @@ -1,8 +1,7 @@ import superjson from "superjson"; -import { createId, hashObjectBase64 } from "@homarr/common"; +import { createId } from "@homarr/common"; import { createLogger } from "@homarr/core/infrastructure/logs"; -import type { WidgetKind } from "@homarr/definitions"; import { ChannelSubscriptionTracker } from "./channel-subscription-tracker"; import { createRedisConnection } from "./connection"; @@ -129,218 +128,38 @@ export const createGetSetChannel = (name: string) => { }; }; -interface QueryCacheChannelOptions { - userId: string; - boardId: string; - key: string; - ttlMs: number; - maxValueBytes: number; -} +const queryCacheKey = (userId: string, boardId: string) => `qc:${userId}:${boardId}`; -const queryCacheHashKey = (userId: string, boardId: string) => `qc:${userId}:${boardId}`; - -export const createQueryCacheChannel = ({ userId, boardId, key, ttlMs, maxValueBytes }: QueryCacheChannelOptions) => { - const hkey = queryCacheHashKey(userId, boardId); - - return { - name: `${hkey}:${key}`, - getAsync: async () => { - return await getSetClient.hget(hkey, key); - }, - setAsync: async (value: string) => { - if (Buffer.byteLength(value, "utf8") > maxValueBytes) { - logger.warn("Query cache value exceeded maximum size", { - channel: hkey, - valueBytes: Buffer.byteLength(value, "utf8"), - maxValueBytes, - }); - return false; - } - - await getSetClient.hset(hkey, key, value); - await getSetClient.pexpire(hkey, ttlMs); - return true; - }, - removeAsync: async () => { - await getSetClient.hdel(hkey, key); - }, - }; -}; - -export const getAllQueryCacheAsync = async (userId: string, boardId: string): Promise> => { - const hkey = queryCacheHashKey(userId, boardId); - return await getSetClient.hgetall(hkey); -}; - -/** - * Creates a new cache channel. - * @param name name of the channel - * @param cacheDurationMs duration in milliseconds to cache - * @returns cache channel object - */ -export const createCacheChannel = (name: string, cacheDurationMs: number = 5 * 60 * 1000) => { - const cacheChannelName = `cache:${name}`; - - return { - /** - * Get the data from the cache channel. - * @returns data or null if not found or expired - */ - getAsync: async () => { - const data = await getSetClient.get(cacheChannelName); - if (!data) return null; - - const parsedData = superjson.parse<{ data: TData; timestamp: Date }>(data); - const now = new Date(); - const diff = now.getTime() - parsedData.timestamp.getTime(); - if (diff > cacheDurationMs) return null; - - return parsedData; - }, - /** - * Consume the data from the cache channel, if not present or expired, it will call the callback to get new data. - * @param callback callback function to get new data if not present or expired - * @returns data or new data if not present or expired - */ - consumeAsync: async (callback: () => Promise) => { - const data = await getSetClient.get(cacheChannelName); - - const getNewDataAsync = async () => { - logger.debug(`Cache miss for channel '${cacheChannelName}'`); - const newData = await callback(); - const result = { data: newData, timestamp: new Date() }; - await getSetClient.set(cacheChannelName, superjson.stringify(result)); - logger.debug(`Cache updated for channel '${cacheChannelName}'`); - return result; - }; - - if (!data) { - return await getNewDataAsync(); - } - - const parsedData = superjson.parse<{ data: TData; timestamp: Date }>(data); - const now = new Date(); - const diff = now.getTime() - parsedData.timestamp.getTime(); - - if (diff > cacheDurationMs) { - return await getNewDataAsync(); - } - - logger.debug(`Cache hit for channel '${cacheChannelName}'`); - - return parsedData; - }, - /** - * Invalidate the cache channels data. - */ - invalidateAsync: async () => { - await getSetClient.del(cacheChannelName); - }, - /** - * Set the data in the cache channel. - * @param data data to be stored in the cache channel - */ - setAsync: async (data: TData) => { - await getSetClient.set(cacheChannelName, superjson.stringify({ data, timestamp: new Date() })); - }, - }; -}; - -export const createItemAndIntegrationChannel = (kind: WidgetKind, integrationId: string) => { - const channelName = `item:${kind}:integration:${integrationId}`; - return createChannelWithLatestAndEvents(channelName); -}; - -export const createIntegrationOptionsChannel = ( - integrationId: string, - queryKey: string, - options: Record, +export const setQueryCacheAsync = async ( + userId: string, + boardId: string, + value: string, + ttlMs: number, + maxValueBytes: number, ) => { - const optionsKey = hashObjectBase64(options); - const channelName = `integration:${integrationId}:${queryKey}:options:${optionsKey}`; - return createChannelWithLatestAndEvents(channelName); + if (Buffer.byteLength(value, "utf8") > maxValueBytes) { + logger.warn("Query cache value exceeded maximum size", { + key: queryCacheKey(userId, boardId), + valueBytes: Buffer.byteLength(value, "utf8"), + maxValueBytes, + }); + return false; + } + await getSetClient.set(queryCacheKey(userId, boardId), value, "PX", ttlMs); + return true; +}; + +export const getQueryCacheAsync = async (userId: string, boardId: string) => + await getSetClient.get(queryCacheKey(userId, boardId)); + +export const removeQueryCacheAsync = async (userId: string, boardId: string) => { + await getSetClient.del(queryCacheKey(userId, boardId)); }; -/** - * Invalidates all cached data for a given integration by deleting every Redis key - * that starts with `integration:${integrationId}:` as well as the integration's - * session store entry. This ensures that stale data from a misconfigured or - * updated integration is never served from cache. - */ export const invalidateIntegrationCacheAsync = async (integrationId: string): Promise => { const client = getSetClient as typeof getSetClient | null; - if (!client) { - return; - } - - const patterns = [`integration:${integrationId}:*`, `session-store:${integrationId}`]; - let deletedCount = 0; - for (const pattern of patterns) { - let cursor = "0"; - do { - const [nextCursor, keys] = await client.scan(cursor, "MATCH", pattern, "COUNT", 200); - cursor = nextCursor; - if (keys.length > 0) { - await client.del(...keys); - deletedCount += keys.length; - } - } while (cursor !== "0"); - } - if (deletedCount > 0) { - logger.info("Invalidated integration cache", { integrationId, deletedKeys: deletedCount }); - } -}; - -export const createWidgetOptionsChannel = ( - widgetKind: WidgetKind, - queryKey: string, - options: Record, -) => { - const optionsKey = hashObjectBase64(options); - const channelName = `widget:${widgetKind}:${queryKey}:options:${optionsKey}`; - return createChannelWithLatestAndEvents(channelName); -}; - -export const createItemChannel = (itemId: string) => { - return createChannelWithLatestAndEvents(`item:${itemId}`); -}; - -export const createChannelEventHistory = (channelName: string, maxElements = 32) => { - return { - subscribe: (callback: (data: TData) => void) => { - return ChannelSubscriptionTracker.subscribe(channelName, (message) => { - callback(superjson.parse(message)); - }); - }, - pushAsync: async (data: TData, options = { publish: false }) => { - if (options.publish) await publisher.publish(channelName, superjson.stringify(data)); - await getSetClient.lpush(channelName, superjson.stringify({ data, timestamp: new Date() })); - await getSetClient.ltrim(channelName, 0, maxElements); - }, - clearAsync: async () => { - await getSetClient.del(channelName); - }, - /** - * Returns a slice of the available data in the channel. - * If any of the indexes are out of range (or -range), returned data will be clamped. - * @param startIndex Start index of the slice, negative values are counted from the end, defaults at beginning of range. - * @param endIndex End index of the slice, negative values are counted from the end, defaults at end of range. - */ - getSliceAsync: async (startIndex = 0, endIndex = -1) => { - const range = await getSetClient.lrange(channelName, startIndex, endIndex); - return range.map((item) => superjson.parse<{ data: TData; timestamp: Date }>(item)); - }, - getSliceUntilTimeAsync: async (time: Date) => { - const itemsInCollection = await getSetClient.lrange(channelName, 0, -1); - return itemsInCollection - .map((item) => superjson.parse<{ data: TData; timestamp: Date }>(item)) - .filter((item) => item.timestamp < time); - }, - getLengthAsync: async () => { - return await getSetClient.llen(channelName); - }, - name: channelName, - }; + if (!client) return; + await client.del(`session-store:${integrationId}`); }; /** @@ -407,40 +226,6 @@ export const createChannelEventHistoryOld = (channelName: string, maxElem }; }; -export const createChannelWithLatestAndEvents = (channelName: string) => { - return { - subscribe: (callback: (data: TData) => void) => { - return ChannelSubscriptionTracker.subscribe(channelName, (message) => { - callback(superjson.parse(message)); - }); - }, - publishAndUpdateLastStateAsync: async (data: TData) => { - // Set first, then publish. If publish fails the storage is still consistent - // and subscribers will catch up on the next event. - const payload = superjson.stringify({ data, timestamp: new Date() }); - await getSetClient.set(channelName, payload); - try { - await publisher.publish(channelName, superjson.stringify(data)); - } catch (error) { - logger.warn("Failed to publish cache update (storage is correct, subscribers will catch up)", { - channel: channelName, - error: error instanceof Error ? error.message : String(error), - }); - } - }, - setAsync: async (data: TData) => { - await getSetClient.set(channelName, superjson.stringify({ data, timestamp: new Date() })); - }, - getAsync: async () => { - const data = await getSetClient.get(channelName); - if (!data) return null; - - return superjson.parse<{ data: TData; timestamp: Date }>(data); - }, - name: channelName, - }; -}; - const queueClient = createRedisConnection(); type WithId = TItem & { _id: string }; diff --git a/packages/redis/src/lib/channels/history-channel.ts b/packages/redis/src/lib/channels/history-channel.ts deleted file mode 100644 index 541830464..000000000 --- a/packages/redis/src/lib/channels/history-channel.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createChannelEventHistory } from "../channel"; - -export const createIntegrationHistoryChannel = (integrationId: string, queryKey: string, maxElements = 32) => { - const channelName = `integration:${integrationId}:history:${queryKey}`; - return createChannelEventHistory(channelName, maxElements); -}; diff --git a/packages/request-handler/src/anchor-notes.ts b/packages/request-handler/src/anchor-notes.ts index bd6f19e44..75677b8ba 100644 --- a/packages/request-handler/src/anchor-notes.ts +++ b/packages/request-handler/src/anchor-notes.ts @@ -1,30 +1,22 @@ -import dayjs from "dayjs"; - import type { AnchorNote, AnchorNotesListInput, AnchorNoteSummary } from "@homarr/integrations"; import { createIntegrationAsync } from "@homarr/integrations"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const anchorNotesListRequestHandler = createCachedIntegrationRequestHandler< +export const anchorNotesListRequestHandler = createIntegrationRequestHandler< AnchorNoteSummary[], "anchor", AnchorNotesListInput >({ - queryKey: "anchorNotesList", - cacheDuration: dayjs.duration(15, "seconds"), async requestAsync(integration, input) { const instance = await createIntegrationAsync(integration); return instance.listNotesAsync(input); }, }); -export const anchorNoteRequestHandler = createCachedIntegrationRequestHandler( - { - queryKey: "anchorNote", - cacheDuration: dayjs.duration(15, "seconds"), - async requestAsync(integration, input) { - const instance = await createIntegrationAsync(integration); - return instance.getNoteAsync(input.noteId); - }, +export const anchorNoteRequestHandler = createIntegrationRequestHandler({ + async requestAsync(integration, input) { + const instance = await createIntegrationAsync(integration); + return instance.getNoteAsync(input.noteId); }, -); +}); diff --git a/packages/request-handler/src/archive-team-warrior.ts b/packages/request-handler/src/archive-team-warrior.ts index 4038ec9dc..e02391f19 100644 --- a/packages/request-handler/src/archive-team-warrior.ts +++ b/packages/request-handler/src/archive-team-warrior.ts @@ -1,12 +1,10 @@ -import dayjs from "dayjs"; - import type { IntegrationKindByCategory } from "@homarr/definitions"; import type { ArchiveTeamWarriorStatus } from "@homarr/integrations"; import { createIntegrationAsync } from "@homarr/integrations"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const archiveTeamWarriorRequestHandler = createCachedIntegrationRequestHandler< +export const archiveTeamWarriorRequestHandler = createIntegrationRequestHandler< ArchiveTeamWarriorStatus, IntegrationKindByCategory<"archiving">, Record @@ -15,6 +13,4 @@ export const archiveTeamWarriorRequestHandler = createCachedIntegrationRequestHa const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getStatusAsync(); }, - cacheDuration: dayjs.duration(5, "minute"), - queryKey: "archiveTeamWarriorStatus", }); diff --git a/packages/request-handler/src/audiobookshelf.ts b/packages/request-handler/src/audiobookshelf.ts index bb4b2d83c..12562342c 100644 --- a/packages/request-handler/src/audiobookshelf.ts +++ b/packages/request-handler/src/audiobookshelf.ts @@ -1,11 +1,9 @@ -import dayjs from "dayjs"; - import { createIntegrationAsync } from "@homarr/integrations"; import type { AudiobookshelfDashboardData } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const audiobookshelfRequestHandler = createCachedIntegrationRequestHandler< +export const audiobookshelfRequestHandler = createIntegrationRequestHandler< AudiobookshelfDashboardData, "audiobookshelf", Record @@ -14,6 +12,4 @@ export const audiobookshelfRequestHandler = createCachedIntegrationRequestHandle const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getDashboardDataAsync(); }, - cacheDuration: dayjs.duration(10, "minutes"), - queryKey: "audiobookshelfDashboard", }); diff --git a/packages/request-handler/src/bazarr.ts b/packages/request-handler/src/bazarr.ts new file mode 100644 index 000000000..56b391658 --- /dev/null +++ b/packages/request-handler/src/bazarr.ts @@ -0,0 +1,15 @@ +import { createIntegrationAsync } from "@homarr/integrations"; +import type { BazarrBadges } from "@homarr/integrations"; + +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; + +export const bazarrBadgesRequestHandler = createIntegrationRequestHandler< + BazarrBadges, + "bazarr", + Record +>({ + async requestAsync(integration) { + const integrationInstance = await createIntegrationAsync(integration); + return await integrationInstance.getBadgesAsync(); + }, +}); diff --git a/packages/request-handler/src/beszel.ts b/packages/request-handler/src/beszel.ts index 6dbe20ae0..dab263e3a 100644 --- a/packages/request-handler/src/beszel.ts +++ b/packages/request-handler/src/beszel.ts @@ -1,5 +1,3 @@ -import dayjs from "dayjs"; - import { createLogger } from "@homarr/core/infrastructure/logs"; import { createIntegrationAsync } from "@homarr/integrations"; import type { @@ -12,7 +10,7 @@ import type { BeszelSystemStatsRecord, } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; const logger = createLogger({ module: "beszelRequestHandler" }); @@ -36,6 +34,7 @@ function mapToSystemRow(system: BeszelSystem, details: BeszelSystemDetails | nul cpu: info.cpu, memory: info.mp, disk: info.dp, + extraFilesystems: info.efs ?? {}, gpu: info.g ?? 0, loadAvg: info.la ?? null, // bb = bytes/s (newer), b = Mbps (legacy, multiply to get bytes/s) @@ -53,7 +52,7 @@ function mapToSystemRow(system: BeszelSystem, details: BeszelSystemDetails | nul }; } -export const beszelSystemsRequestHandler = createCachedIntegrationRequestHandler< +export const beszelSystemsRequestHandler = createIntegrationRequestHandler< BeszelSystemRow[], "beszel" | "mock", Record @@ -64,7 +63,10 @@ export const beszelSystemsRequestHandler = createCachedIntegrationRequestHandler const systems = await instance.getSystemsAsync(); const enriched = await Promise.all( systems.map(async (system) => { - const details = await instance.getSystemDetailsAsync(system.id).catch(() => null); + const details = await instance.getSystemDetailsAsync(system.id).catch((error) => { + logger.warn("Failed to fetch Beszel system details", { systemId: system.id, error: String(error) }); + return null; + }); return mapToSystemRow(system, details); }), ); @@ -75,10 +77,6 @@ export const beszelSystemsRequestHandler = createCachedIntegrationRequestHandler }); return enriched; }, - cacheDuration: dayjs.duration(1, "second"), - fallbackToStaleOnError: true, - retry: { attempts: 2, delayMs: 500 }, - queryKey: "beszelSystems", }); export interface BeszelAlertsData { @@ -86,7 +84,7 @@ export interface BeszelAlertsData { history: BeszelAlertHistory[]; } -export const beszelAlertsRequestHandler = createCachedIntegrationRequestHandler< +export const beszelAlertsRequestHandler = createIntegrationRequestHandler< BeszelAlertsData, "beszel" | "mock", { includeHistory: boolean; maxHistoryItems: number } @@ -104,10 +102,6 @@ export const beszelAlertsRequestHandler = createCachedIntegrationRequestHandler< }); return { alerts, history }; }, - cacheDuration: dayjs.duration(1, "second"), - fallbackToStaleOnError: true, - retry: { attempts: 2, delayMs: 500 }, - queryKey: "beszelAlerts", }); const timePeriodConfig: Record = { @@ -124,18 +118,24 @@ export interface BeszelStatsData { containerStats: BeszelContainerStatsRecord[]; } -export const beszelStatsRequestHandler = createCachedIntegrationRequestHandler< +export const beszelStatsRequestHandler = createIntegrationRequestHandler< BeszelStatsData, "beszel" | "mock", { systemId: string; timePeriod: string; includeDocker: boolean } >({ + // No cache — the widget polls every 5s for live updates; + // a TTL here would serve stale records between polls. + cacheTtlMs: 0, async requestAsync(integration, input) { const start = performance.now(); const config = timePeriodConfig[input.timePeriod] ?? { type: "1m", perPage: 60 }; const instance = await createIntegrationAsync(integration); const systemStats = await instance.getSystemStatsAsync(input.systemId, config.type, config.perPage); const containerStats = input.includeDocker - ? await instance.getContainerStatsAsync(input.systemId, config.type, config.perPage).catch(() => []) + ? await instance.getContainerStatsAsync(input.systemId, config.type, config.perPage).catch((error) => { + logger.warn("Failed to fetch Beszel container stats", { systemId: input.systemId, error: String(error) }); + return []; + }) : []; logger.debug("beszelStats fetch completed", { integrationId: integration.id, @@ -147,8 +147,4 @@ export const beszelStatsRequestHandler = createCachedIntegrationRequestHandler< }); return { systemStats, containerStats }; }, - cacheDuration: dayjs.duration(1, "second"), - fallbackToStaleOnError: true, - retry: { attempts: 2, delayMs: 500 }, - queryKey: "beszelStats", }); diff --git a/packages/request-handler/src/calendar.ts b/packages/request-handler/src/calendar.ts index dd198912a..a358d13e8 100644 --- a/packages/request-handler/src/calendar.ts +++ b/packages/request-handler/src/calendar.ts @@ -4,9 +4,9 @@ import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { CalendarEvent, RadarrReleaseType } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const calendarMonthRequestHandler = createCachedIntegrationRequestHandler< +export const calendarMonthRequestHandler = createIntegrationRequestHandler< CalendarEvent[], IntegrationKindByCategory<"calendar">, { year: number; month: number; releaseType: RadarrReleaseType[]; showUnmonitored: boolean } @@ -23,6 +23,4 @@ export const calendarMonthRequestHandler = createCachedIntegrationRequestHandler input.showUnmonitored, ); }, - cacheDuration: dayjs.duration(1, "minute"), - queryKey: "calendarMonth", }); diff --git a/packages/request-handler/src/coolify.ts b/packages/request-handler/src/coolify.ts index d307a780f..25ddfe962 100644 --- a/packages/request-handler/src/coolify.ts +++ b/packages/request-handler/src/coolify.ts @@ -1,11 +1,9 @@ -import dayjs from "dayjs"; - import { createIntegrationAsync } from "@homarr/integrations"; import type { CoolifyInstanceInfo } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const coolifyRequestHandler = createCachedIntegrationRequestHandler< +export const coolifyRequestHandler = createIntegrationRequestHandler< CoolifyInstanceInfo, "coolify", Record @@ -14,6 +12,4 @@ export const coolifyRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getInstanceInfoAsync(); }, - cacheDuration: dayjs.duration(30, "seconds"), - queryKey: "coolifyInfo", }); diff --git a/packages/request-handler/src/dns-hole.ts b/packages/request-handler/src/dns-hole.ts index a8c920801..85188c740 100644 --- a/packages/request-handler/src/dns-hole.ts +++ b/packages/request-handler/src/dns-hole.ts @@ -1,12 +1,10 @@ -import dayjs from "dayjs"; - import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { DnsHoleSummary } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const dnsHoleRequestHandler = createCachedIntegrationRequestHandler< +export const dnsHoleRequestHandler = createIntegrationRequestHandler< DnsHoleSummary, IntegrationKindByCategory<"dnsHole">, Record @@ -15,6 +13,4 @@ export const dnsHoleRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getSummaryAsync(); }, - cacheDuration: dayjs.duration(5, "seconds"), - queryKey: "dnsHoleSummary", }); diff --git a/packages/request-handler/src/docker.ts b/packages/request-handler/src/docker.ts index 2d228a598..a238b4a3c 100644 --- a/packages/request-handler/src/docker.ts +++ b/packages/request-handler/src/docker.ts @@ -1,5 +1,4 @@ import type { Readable } from "node:stream"; -import dayjs from "dayjs"; import type { Container, ContainerInfo, ContainerStats } from "dockerode"; import type Dockerode from "dockerode"; @@ -13,7 +12,7 @@ import type { ContainerState, Port } from "@homarr/docker"; import { dockerLabels, DockerSingleton } from "@homarr/docker"; import { createDockerLogStreamProcessor, decodeDockerLogs } from "./docker-log-decode"; -import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; +import { createWidgetRequestHandler } from "./lib/widget-request-handler"; const logger = createLogger({ module: "dockerRequestHandler" }); @@ -171,16 +170,13 @@ const mockContainers: { }, ]; -export const dockerContainersRequestHandler = createCachedWidgetRequestHandler({ - queryKey: "dockerContainersResult", - widgetKind: "dockerContainers", +export const dockerContainersRequestHandler = createWidgetRequestHandler({ async requestAsync() { if (isDemoMode) { return mockContainers; } return await getContainersWithStatsAsync(); }, - cacheDuration: dayjs.duration(20, "seconds"), }); const extractImage = (container: ContainerInfo) => extractContainerImageName(container.Image); @@ -304,7 +300,10 @@ async function getContainersWithStatsAsync() { const stats = await instance .getContainer(container.Id) - .stats({ stream: false, "one-shot": true }) + // Not in one-shot mode: the daemon then includes precpu_stats (the + // previous reading), which is required to compute the current CPU usage + // as a delta. One-shot omits it and would only expose lifetime totals. + .stats({ stream: false }) .catch( () => ({ @@ -340,12 +339,22 @@ export function calculateCpuUsage(stats: ContainerStats): number { } const numberOfCpus = stats.cpu_stats.online_cpus; - const usage = stats.cpu_stats.system_cpu_usage; - if (!usage || usage === 0) { + + // Docker's own formula for the current (instantaneous) CPU usage: it compares + // the latest reading against the previous one (precpu_stats). Dividing the + // cumulative total_usage by the cumulative system_cpu_usage instead would + // report the container's lifetime average, which stays high long after a load + // spike has ended. precpu_stats is only present when the stats are NOT fetched + // in one-shot mode; when it is absent (e.g. Podman) the deltas fall back to the + // cumulative totals, matching the previous behaviour. + const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - (stats.precpu_stats?.cpu_usage?.total_usage ?? 0); + const systemDelta = (stats.cpu_stats.system_cpu_usage ?? 0) - (stats.precpu_stats?.system_cpu_usage ?? 0); + + if (systemDelta <= 0 || cpuDelta < 0) { return 0; } - return (stats.cpu_stats.cpu_usage.total_usage / usage) * numberOfCpus * 100; + return (cpuDelta / systemDelta) * numberOfCpus * 100; } export function calculateMemoryUsage(stats: ContainerStats): number { diff --git a/packages/request-handler/src/downloads.ts b/packages/request-handler/src/downloads.ts index 06d4e9c64..12106b07a 100644 --- a/packages/request-handler/src/downloads.ts +++ b/packages/request-handler/src/downloads.ts @@ -1,12 +1,10 @@ -import dayjs from "dayjs"; - import type { IntegrationKindByCategory } from "@homarr/definitions"; import type { DownloadClientJobsAndStatus } from "@homarr/integrations"; import { createIntegrationAsync } from "@homarr/integrations"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const downloadClientRequestHandler = createCachedIntegrationRequestHandler< +export const downloadClientRequestHandler = createIntegrationRequestHandler< DownloadClientJobsAndStatus, IntegrationKindByCategory<"downloadClient">, { limit: number } @@ -15,6 +13,4 @@ export const downloadClientRequestHandler = createCachedIntegrationRequestHandle const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getClientJobsAndStatusAsync(input); }, - cacheDuration: dayjs.duration(5, "seconds"), - queryKey: "downloadClientJobStatus", }); diff --git a/packages/request-handler/src/duckduckgo-bangs.ts b/packages/request-handler/src/duckduckgo-bangs.ts index a51bf5327..628e125a1 100644 --- a/packages/request-handler/src/duckduckgo-bangs.ts +++ b/packages/request-handler/src/duckduckgo-bangs.ts @@ -1,12 +1,10 @@ -import dayjs from "dayjs"; import { z } from "zod"; import { env } from "@homarr/common/env"; import { ResponseError } from "@homarr/common/server"; import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; -import { createChannelWithLatestAndEvents } from "@homarr/redis"; -import { createCachedRequestHandler } from "@homarr/request-handler/lib/cached-request-handler"; +import { createRequestHandler } from "@homarr/request-handler/lib/request-handler"; export type DuckDuckGoBang = z.infer; @@ -38,9 +36,7 @@ const normalizeBangToken = (token: string) => token.toLowerCase().trim(); const compareBangTokenAsc = (left: DuckDuckGoBang, right: DuckDuckGoBang) => left.t.localeCompare(right.t); -export const duckDuckGoBangsRequestHandler = createCachedRequestHandler({ - queryKey: "duckduckgo-bangs", - cacheDuration: dayjs.duration(1, "day"), +export const duckDuckGoBangsRequestHandler = createRequestHandler({ async requestAsync(_) { if (env.NO_EXTERNAL_CONNECTION) { return []; @@ -73,7 +69,4 @@ export const duckDuckGoBangsRequestHandler = createCachedRequestHandler({ return normalized; }, - createRedisChannel() { - return createChannelWithLatestAndEvents("homarr:duckduckgo-bangs"); - }, }); diff --git a/packages/request-handler/src/firewall.ts b/packages/request-handler/src/firewall.ts index 3754f1099..ad4f25b70 100644 --- a/packages/request-handler/src/firewall.ts +++ b/packages/request-handler/src/firewall.ts @@ -1,5 +1,3 @@ -import dayjs from "dayjs"; - import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { @@ -9,9 +7,9 @@ import type { FirewallVersionSummary, } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const firewallCpuRequestHandler = createCachedIntegrationRequestHandler< +export const firewallCpuRequestHandler = createIntegrationRequestHandler< FirewallCpuSummary, IntegrationKindByCategory<"firewall">, Record @@ -20,11 +18,9 @@ export const firewallCpuRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return integrationInstance.getFirewallCpuAsync(); }, - cacheDuration: dayjs.duration(5, "seconds"), - queryKey: "firewallCpuSummary", }); -export const firewallMemoryRequestHandler = createCachedIntegrationRequestHandler< +export const firewallMemoryRequestHandler = createIntegrationRequestHandler< FirewallMemorySummary, IntegrationKindByCategory<"firewall">, Record @@ -33,11 +29,9 @@ export const firewallMemoryRequestHandler = createCachedIntegrationRequestHandle const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getFirewallMemoryAsync(); }, - cacheDuration: dayjs.duration(15, "seconds"), - queryKey: "firewallMemorySummary", }); -export const firewallInterfacesRequestHandler = createCachedIntegrationRequestHandler< +export const firewallInterfacesRequestHandler = createIntegrationRequestHandler< FirewallInterfacesSummary[], IntegrationKindByCategory<"firewall">, Record @@ -46,11 +40,9 @@ export const firewallInterfacesRequestHandler = createCachedIntegrationRequestHa const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getFirewallInterfacesAsync(); }, - cacheDuration: dayjs.duration(30, "seconds"), - queryKey: "firewallInterfacesSummary", }); -export const firewallVersionRequestHandler = createCachedIntegrationRequestHandler< +export const firewallVersionRequestHandler = createIntegrationRequestHandler< FirewallVersionSummary, IntegrationKindByCategory<"firewall">, Record @@ -59,6 +51,4 @@ export const firewallVersionRequestHandler = createCachedIntegrationRequestHandl const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getFirewallVersionAsync(); }, - cacheDuration: dayjs.duration(1, "hour"), - queryKey: "firewallVersionSummary", }); diff --git a/packages/request-handler/src/health-monitoring.ts b/packages/request-handler/src/health-monitoring.ts index 640eb6b97..959d3e0ed 100644 --- a/packages/request-handler/src/health-monitoring.ts +++ b/packages/request-handler/src/health-monitoring.ts @@ -1,25 +1,21 @@ -import dayjs from "dayjs"; - import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { ProxmoxClusterInfo, SystemHealthMonitoring } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const systemInfoRequestHandler = createCachedIntegrationRequestHandler< +export const systemInfoRequestHandler = createIntegrationRequestHandler< SystemHealthMonitoring, - Exclude, "proxmox" | "coolify" | "beszel">, + Exclude, "proxmox" | "coolify" | "beszel" | "patchmon">, Record >({ async requestAsync(integration, _input) { const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getSystemInfoAsync(); }, - cacheDuration: dayjs.duration(5, "seconds"), - queryKey: "systemInfo", }); -export const clusterInfoRequestHandler = createCachedIntegrationRequestHandler< +export const clusterInfoRequestHandler = createIntegrationRequestHandler< ProxmoxClusterInfo, "proxmox" | "mock", Record @@ -28,6 +24,4 @@ export const clusterInfoRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getClusterInfoAsync(); }, - cacheDuration: dayjs.duration(5, "seconds"), - queryKey: "clusterInfo", }); diff --git a/packages/request-handler/src/immich.ts b/packages/request-handler/src/immich.ts index e95cb32b8..56fc89bd0 100644 --- a/packages/request-handler/src/immich.ts +++ b/packages/request-handler/src/immich.ts @@ -1,12 +1,10 @@ -import dayjs from "dayjs"; - import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { ImmichAlbum, ImmichServerStats } from "@homarr/integrations"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const immichStatsRequestHandler = createCachedIntegrationRequestHandler< +export const immichStatsRequestHandler = createIntegrationRequestHandler< ImmichServerStats, IntegrationKindByCategory<"photoService">, Record @@ -15,11 +13,9 @@ export const immichStatsRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getServerStatsAsync(); }, - cacheDuration: dayjs.duration(15, "minute"), - queryKey: "immich-server-stats", }); -export const immichAlbumsRequestHandler = createCachedIntegrationRequestHandler< +export const immichAlbumsRequestHandler = createIntegrationRequestHandler< { id: string; albumName: string; @@ -32,11 +28,9 @@ export const immichAlbumsRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getAlbumsAsync(); }, - cacheDuration: dayjs.duration(15, "minute"), - queryKey: "immich-albums", }); -export const immichAlbumRequestHandler = createCachedIntegrationRequestHandler< +export const immichAlbumRequestHandler = createIntegrationRequestHandler< ImmichAlbum, IntegrationKindByCategory<"photoService">, { albumId: string } @@ -45,6 +39,4 @@ export const immichAlbumRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getAlbumAsync(input.albumId); }, - cacheDuration: dayjs.duration(15, "minute"), - queryKey: "immich-album", }); diff --git a/packages/request-handler/src/indexer-manager.ts b/packages/request-handler/src/indexer-manager.ts index a957b32ad..f21a61f8f 100644 --- a/packages/request-handler/src/indexer-manager.ts +++ b/packages/request-handler/src/indexer-manager.ts @@ -1,12 +1,10 @@ -import dayjs from "dayjs"; - import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { Indexer } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const indexerManagerRequestHandler = createCachedIntegrationRequestHandler< +export const indexerManagerRequestHandler = createIntegrationRequestHandler< Indexer[], IntegrationKindByCategory<"indexerManager">, Record @@ -15,6 +13,4 @@ export const indexerManagerRequestHandler = createCachedIntegrationRequestHandle const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getIndexersAsync(); }, - cacheDuration: dayjs.duration(5, "minutes"), - queryKey: "indexerManager", }); diff --git a/packages/request-handler/src/lib/cached-integration-request-handler.ts b/packages/request-handler/src/lib/cached-integration-request-handler.ts deleted file mode 100644 index aee69e39c..000000000 --- a/packages/request-handler/src/lib/cached-integration-request-handler.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { Duration } from "dayjs/plugin/duration"; - -import type { Modify } from "@homarr/common/types"; -import type { Integration, IntegrationSecret } from "@homarr/db/schema"; -import type { IntegrationKind } from "@homarr/definitions"; -import { createIntegrationOptionsChannel } from "@homarr/redis"; - -import { createCachedRequestHandler } from "./cached-request-handler"; - -type IntegrationOfKind = Omit & { - kind: TKind; - decryptedSecrets: Modify, { value: string }>[]; - externalUrl: string | null; -}; - -interface Options> { - queryKey: string; - requestAsync: (integration: IntegrationOfKind, input: TInput) => Promise; - cacheDuration: Duration; - cacheDurationForInput?: (input: TInput) => Duration | undefined; - fallbackToStaleOnError?: boolean; - retry?: { attempts?: number; delayMs?: number }; - isValid?: (data: TData) => boolean; -} - -export const createCachedIntegrationRequestHandler = < - TData, - TKind extends IntegrationKind, - TInput extends Record, ->( - options: Options, -) => { - return { - handler: (integration: IntegrationOfKind, itemOptions: TInput) => - createCachedRequestHandler({ - queryKey: options.queryKey, - requestAsync: async (input: { options: TInput; integration: IntegrationOfKind }) => { - return await options.requestAsync(input.integration, input.options); - }, - cacheDuration: options.cacheDurationForInput?.(itemOptions) ?? options.cacheDuration, - fallbackToStaleOnError: options.fallbackToStaleOnError, - retry: options.retry, - isValid: options.isValid, - createRedisChannel(input, handlerOptions) { - return createIntegrationOptionsChannel(input.integration.id, handlerOptions.queryKey, input.options); - }, - }).handler({ options: itemOptions, integration }), - }; -}; diff --git a/packages/request-handler/src/lib/cached-request-handler.ts b/packages/request-handler/src/lib/cached-request-handler.ts deleted file mode 100644 index c2342eed3..000000000 --- a/packages/request-handler/src/lib/cached-request-handler.ts +++ /dev/null @@ -1,160 +0,0 @@ -import dayjs from "dayjs"; -import type { Duration } from "dayjs/plugin/duration"; - -import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; -import { createLogger } from "@homarr/core/infrastructure/logs"; -import type { createChannelWithLatestAndEvents } from "@homarr/redis"; - -const logger = createLogger({ module: "cachedRequestHandler" }); - -type FetchResult = { data: TData; timestamp: Date }; -type InFlightEntry = Promise>; - -const inFlightFetches = new Map(); - -const isAbortedError = (error: unknown): boolean => { - if (!(error instanceof Error)) return false; - return error.name === "AbortError" || error.name === "TimeoutError"; -}; - -interface Options> { - // Unique key for this request handler - queryKey: string; - requestAsync: (input: TInput) => Promise; - createRedisChannel: ( - input: TInput, - options: Options, - ) => ReturnType>; - cacheDuration: Duration; - fallbackToStaleOnError?: boolean; - // Retry the upstream call on failure. attempts=1 means no retry (default). - // AbortError / TimeoutError are never retried. - retry?: { attempts?: number; delayMs?: number }; - // Predicate to decide whether a fetched response is worth caching. - // Return false to skip the cache write (next call will refetch). - isValid?: (data: TData) => boolean; -} - -const defaultRetry = { attempts: 1, delayMs: 0 }; -const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - -export const createCachedRequestHandler = >( - options: Options, -) => { - return { - handler: (input: TInput) => { - const channel = options.createRedisChannel(input, options); - const retryConfig = { ...defaultRetry, ...options.retry }; - - return { - async getCachedOrUpdatedDataAsync({ forceUpdate = false }) { - const channelData = await channel.getAsync(); - - const requestNewDataAsync = (): Promise> => { - const inflight = inFlightFetches.get(channel.name); - if (inflight) { - logger.debug("Cached request handler joining in-flight fetch", { - channel: channel.name, - queryKey: options.queryKey, - }); - return inflight as Promise>; - } - - const promise = (async (): Promise> => { - let lastError: unknown; - for (let attempt = 1; attempt <= retryConfig.attempts; attempt++) { - try { - const data = await options.requestAsync(input); - if (options.isValid && !options.isValid(data)) { - logger.warn("Cached request handler received invalid data, not caching", { - channel: channel.name, - queryKey: options.queryKey, - attempt, - }); - return { data, timestamp: new Date() }; - } - await channel.publishAndUpdateLastStateAsync(data); - return { data, timestamp: new Date() }; - } catch (error) { - lastError = error; - if (isAbortedError(error) || attempt === retryConfig.attempts) { - break; - } - logger.warn("Cached request handler fetch failed, retrying", { - channel: channel.name, - queryKey: options.queryKey, - attempt, - nextDelayMs: retryConfig.delayMs, - error: error instanceof Error ? `${error.name}: ${error.message}` : String(error), - }); - await sleep(retryConfig.delayMs); - } - } - - if (options.fallbackToStaleOnError && channelData) { - logger.warn( - new ErrorWithMetadata( - "Cached request handler using stale cache after fetch failure", - { - channel: channel.name, - queryKey: options.queryKey, - attempts: retryConfig.attempts, - }, - { cause: lastError }, - ), - ); - return channelData; - } - - throw lastError; - })().finally(() => { - inFlightFetches.delete(channel.name); - }); - - inFlightFetches.set(channel.name, promise as InFlightEntry); - return promise; - }; - - if (forceUpdate) { - logger.debug("Cached request handler forced update", { - channel: channel.name, - queryKey: options.queryKey, - }); - return await requestNewDataAsync(); - } - - const shouldRequestNewData = - !channelData || - dayjs().diff(channelData.timestamp, "milliseconds") > options.cacheDuration.asMilliseconds(); - - if (shouldRequestNewData) { - logger.debug("Cached request handler cache miss", { - channel: channel.name, - queryKey: options.queryKey, - reason: !channelData ? "no data" : "cache expired", - }); - return await requestNewDataAsync(); - } - - logger.debug("Cached request handler cache hit", { - channel: channel.name, - queryKey: options.queryKey, - expiresAt: dayjs(channelData.timestamp).add(options.cacheDuration).toISOString(), - }); - - return channelData; - }, - async invalidateAsync() { - logger.debug("Cached request handler invalidating cache", { - channel: channel.name, - queryKey: options.queryKey, - }); - await this.getCachedOrUpdatedDataAsync({ forceUpdate: true }); - }, - subscribe(callback: (data: TData) => void) { - return channel.subscribe(callback); - }, - }; - }, - }; -}; diff --git a/packages/request-handler/src/lib/cached-request-integration-job-handler.ts b/packages/request-handler/src/lib/cached-request-integration-job-handler.ts deleted file mode 100644 index 63e7682c2..000000000 --- a/packages/request-handler/src/lib/cached-request-integration-job-handler.ts +++ /dev/null @@ -1,115 +0,0 @@ -import SuperJSON from "superjson"; - -import { hashObjectBase64, Stopwatch } from "@homarr/common"; -import { decryptSecret } from "@homarr/common/server"; -import type { MaybeArray } from "@homarr/common/types"; -import { createLogger } from "@homarr/core/infrastructure/logs"; -import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; -import { db } from "@homarr/db"; -import { getItemsWithIntegrationsAsync, getServerSettingsAsync } from "@homarr/db/queries"; -import type { WidgetKind } from "@homarr/definitions"; - -// This imports are done that way to avoid circular dependencies. -import type { inferSupportedIntegrationsStrict } from "../../../widgets/src"; -import { reduceWidgetOptionsWithDefaultValues } from "../../../widgets/src"; -import type { WidgetComponentProps } from "../../../widgets/src/definition"; -import type { createCachedIntegrationRequestHandler } from "./cached-integration-request-handler"; - -const logger = createLogger({ module: "cachedRequestIntegrationJobHandler" }); - -export const createRequestIntegrationJobHandler = < - TWidgetKind extends WidgetKind, - TIntegrationKind extends inferSupportedIntegrationsStrict, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - THandler extends ReturnType>["handler"], ->( - handler: THandler, - { - widgetKinds, - getInput, - }: { - widgetKinds: TWidgetKind[]; - getInput: { - [key in TWidgetKind]: (options: WidgetComponentProps["options"]) => MaybeArray[1]>; - }; - }, -) => { - return async () => { - const serverSettings = await getServerSettingsAsync(db); - const itemsForIntegration = await getItemsWithIntegrationsAsync(db, { - kinds: widgetKinds, - }); - - logger.debug("Found items for integration", { - widgetKinds: widgetKinds.join(","), - count: itemsForIntegration.length, - }); - - const distinctIntegrations: { - integrationId: string; - inputHash: string; - integration: (typeof itemsForIntegration)[number]["integrations"][number]["integration"]; - input: Parameters[1]; - }[] = []; - - for (const itemForIntegration of itemsForIntegration) { - const oneOrMultipleInputs = getInput[itemForIntegration.kind]( - reduceWidgetOptionsWithDefaultValues( - itemForIntegration.kind, - { - enableStatusByDefault: serverSettings.board.enableStatusByDefault, - forceDisableStatus: serverSettings.board.forceDisableStatus, - }, - SuperJSON.parse>(itemForIntegration.options), - ) as never, - ); - for (const { integration } of itemForIntegration.integrations) { - const inputArray = Array.isArray(oneOrMultipleInputs) ? oneOrMultipleInputs : [oneOrMultipleInputs]; - - for (const input of inputArray) { - const inputHash = hashObjectBase64(input); - if ( - distinctIntegrations.some( - (distinctIntegration) => - distinctIntegration.integrationId === integration.id && distinctIntegration.inputHash === inputHash, - ) - ) { - continue; - } - - distinctIntegrations.push({ integrationId: integration.id, inputHash, integration, input }); - } - } - } - - for (const { integrationId, integration, input, inputHash } of distinctIntegrations) { - try { - const decryptedSecrets = integration.secrets.map((secret) => ({ - ...secret, - value: decryptSecret(secret.value), - })); - - const innerHandler = handler( - { - ...integration, - kind: integration.kind as TIntegrationKind, - decryptedSecrets, - externalUrl: integration.app?.href ?? null, - }, - input, - ); - const stopWatch = new Stopwatch(); - await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true }); - logger.debug("Ran integration job", { - integration: integrationId, - inputHash, - elapsed: stopWatch.getElapsedInHumanWords(), - }); - } catch (error) { - logger.error( - new ErrorWithMetadata("Failed to run integration job", { integrationId, inputHash }, { cause: error }), - ); - } - } - }; -}; diff --git a/packages/request-handler/src/lib/cached-widget-request-handler.ts b/packages/request-handler/src/lib/cached-widget-request-handler.ts deleted file mode 100644 index 4016bc5b4..000000000 --- a/packages/request-handler/src/lib/cached-widget-request-handler.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { Duration } from "dayjs/plugin/duration"; - -import type { WidgetKind } from "@homarr/definitions"; -import { createWidgetOptionsChannel } from "@homarr/redis"; - -import { createCachedRequestHandler } from "./cached-request-handler"; - -interface Options> { - // Unique key for this request handler - queryKey: string; - requestAsync: (input: TInput) => Promise; - cacheDuration: Duration; - widgetKind: TKind; -} - -export const createCachedWidgetRequestHandler = < - TData, - TKind extends WidgetKind, - TInput extends Record, ->( - requestHandlerOptions: Options, -) => { - return { - handler: (widgetOptions: TInput) => - createCachedRequestHandler({ - queryKey: requestHandlerOptions.queryKey, - requestAsync: async (input: TInput) => { - return await requestHandlerOptions.requestAsync(input); - }, - cacheDuration: requestHandlerOptions.cacheDuration, - createRedisChannel(input, options) { - return createWidgetOptionsChannel(requestHandlerOptions.widgetKind, options.queryKey, input); - }, - }).handler(widgetOptions), - }; -}; diff --git a/packages/request-handler/src/lib/integration-request-handler.ts b/packages/request-handler/src/lib/integration-request-handler.ts new file mode 100644 index 000000000..e60be1fb8 --- /dev/null +++ b/packages/request-handler/src/lib/integration-request-handler.ts @@ -0,0 +1,44 @@ +import type { Modify } from "@homarr/common/types"; +import type { Integration, IntegrationSecret } from "@homarr/db/schema"; +import type { IntegrationKind } from "@homarr/definitions"; + +import { createRequestHandler } from "./request-handler"; + +type IntegrationOfKind = Omit & { + kind: TKind; + decryptedSecrets: Modify, { value: string }>[]; + externalUrl: string | null; +}; + +interface Options> { + requestAsync: (integration: IntegrationOfKind, input: TInput) => Promise; + cacheTtlMs?: number; +} + +export const createIntegrationRequestHandler = < + TData, + TKind extends IntegrationKind, + TInput extends Record, +>( + options: Options, +) => { + const integrationMap = new Map>(); + const inner = createRequestHandler({ + requestAsync: async (input) => { + const integration = integrationMap.get(input.integrationId); + if (!integration) { + throw new Error(`Integration ${input.integrationId} not found in cache`); + } + return options.requestAsync(integration, input.options); + }, + cacheTtlMs: options.cacheTtlMs, + }); + + return { + invalidateCache: inner.invalidateCache, + handler: (integration: IntegrationOfKind, itemOptions: TInput) => { + integrationMap.set(integration.id, integration); + return inner.handler({ integrationId: integration.id, options: itemOptions }); + }, + }; +}; diff --git a/packages/request-handler/src/lib/request-handler.ts b/packages/request-handler/src/lib/request-handler.ts new file mode 100644 index 000000000..f733078ee --- /dev/null +++ b/packages/request-handler/src/lib/request-handler.ts @@ -0,0 +1,66 @@ +interface Options> { + requestAsync: (input: TInput) => Promise; + cacheTtlMs?: number; +} + +type CacheEntry = { data: TData; timestamp: Date; expiresAt: number }; + +const MAX_CACHE_SIZE = 1000; +const DEFAULT_TTL_MS = 10_000; + +const evictExpired = (cache: Map>) => { + const now = Date.now(); + for (const [key, entry] of cache) { + if (now >= entry.expiresAt) cache.delete(key); + } +}; + +export const createRequestHandler = >( + options: Options, +) => { + const cache = new Map>(); + const inflight = new Map>>(); + + return { + invalidateCache: () => { + cache.clear(); + inflight.clear(); + }, + handler: (input: TInput) => ({ + async getDataAsync(): Promise<{ data: TData; timestamp: Date }> { + const ttl = options.cacheTtlMs ?? DEFAULT_TTL_MS; + const key = JSON.stringify(input); + + const cached = cache.get(key); + if (cached && Date.now() < cached.expiresAt) { + return { data: cached.data, timestamp: cached.timestamp }; + } + if (cached) cache.delete(key); + + const existing = inflight.get(key); + if (existing) return existing; + + const promise = options + .requestAsync(input) + .then((data) => { + if (cache.size >= MAX_CACHE_SIZE) evictExpired(cache); + if (cache.size >= MAX_CACHE_SIZE) { + const oldest = cache.keys().next().value; + if (oldest) cache.delete(oldest); + } + const entry: CacheEntry = { data, timestamp: new Date(), expiresAt: Date.now() + ttl }; + cache.set(key, entry); + inflight.delete(key); + return entry; + }) + .catch((err) => { + inflight.delete(key); + throw err; + }); + + inflight.set(key, promise); + return promise; + }, + }), + }; +}; diff --git a/packages/request-handler/src/lib/widget-request-handler.ts b/packages/request-handler/src/lib/widget-request-handler.ts new file mode 100644 index 000000000..8649791af --- /dev/null +++ b/packages/request-handler/src/lib/widget-request-handler.ts @@ -0,0 +1,14 @@ +import { createRequestHandler } from "./request-handler"; + +interface Options> { + requestAsync: (input: TInput) => Promise; +} + +export const createWidgetRequestHandler = >( + options: Options, +) => ({ + handler: (widgetOptions: TInput) => + createRequestHandler({ + requestAsync: options.requestAsync, + }).handler(widgetOptions), +}); diff --git a/packages/request-handler/src/media-organizer.ts b/packages/request-handler/src/media-organizer.ts new file mode 100644 index 000000000..fbb4d3460 --- /dev/null +++ b/packages/request-handler/src/media-organizer.ts @@ -0,0 +1,33 @@ +import type { IntegrationKindByCategory } from "@homarr/definitions"; +import { createIntegrationAsync } from "@homarr/integrations"; +import type { IMediaOrganizerIntegration, MissingMediaItem, QueuedMediaItem } from "@homarr/integrations/types"; + +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; + +interface MediaOrganizerData { + missing: MissingMediaItem[]; + missingCount: number; + queued: QueuedMediaItem[]; + queuedCount: number; +} + +export const mediaOrganizerRequestHandler = createIntegrationRequestHandler< + MediaOrganizerData, + IntegrationKindByCategory<"mediaOrganizer">, + { pageSize: number } +>({ + async requestAsync(integration, input) { + const integrationInstance = (await createIntegrationAsync(integration)) as unknown as IMediaOrganizerIntegration; + const [missingResult, queueResult] = await Promise.all([ + integrationInstance.getMissingAsync(input.pageSize), + integrationInstance.getMediaQueueAsync(input.pageSize), + ]); + return { + missing: missingResult.items, + missingCount: missingResult.totalCount, + queued: queueResult.items, + queuedCount: queueResult.totalCount, + }; + }, + cacheTtlMs: 60_000, +}); diff --git a/packages/request-handler/src/media-release.ts b/packages/request-handler/src/media-release.ts index c0ffe48f3..02209f68f 100644 --- a/packages/request-handler/src/media-release.ts +++ b/packages/request-handler/src/media-release.ts @@ -1,12 +1,10 @@ -import dayjs from "dayjs"; - import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { MediaRelease } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const mediaReleaseRequestHandler = createCachedIntegrationRequestHandler< +export const mediaReleaseRequestHandler = createIntegrationRequestHandler< MediaRelease[], IntegrationKindByCategory<"mediaRelease">, Record @@ -15,6 +13,4 @@ export const mediaReleaseRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getMediaReleasesAsync(); }, - cacheDuration: dayjs.duration(5, "minutes"), - queryKey: "mediaReleases", }); diff --git a/packages/request-handler/src/media-request-list.ts b/packages/request-handler/src/media-request-list.ts index fd4e02234..62cf29a18 100644 --- a/packages/request-handler/src/media-request-list.ts +++ b/packages/request-handler/src/media-request-list.ts @@ -1,12 +1,10 @@ -import dayjs from "dayjs"; - import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { MediaRequest } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const mediaRequestListRequestHandler = createCachedIntegrationRequestHandler< +export const mediaRequestListRequestHandler = createIntegrationRequestHandler< MediaRequest[], IntegrationKindByCategory<"mediaRequest">, Record @@ -15,6 +13,4 @@ export const mediaRequestListRequestHandler = createCachedIntegrationRequestHand const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getRequestsAsync(); }, - cacheDuration: dayjs.duration(1, "minute"), - queryKey: "mediaRequestList", }); diff --git a/packages/request-handler/src/media-request-stats.ts b/packages/request-handler/src/media-request-stats.ts index a6568aecd..61ff810b8 100644 --- a/packages/request-handler/src/media-request-stats.ts +++ b/packages/request-handler/src/media-request-stats.ts @@ -1,12 +1,10 @@ -import dayjs from "dayjs"; - import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { MediaRequestStats } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const mediaRequestStatsRequestHandler = createCachedIntegrationRequestHandler< +export const mediaRequestStatsRequestHandler = createIntegrationRequestHandler< MediaRequestStats, IntegrationKindByCategory<"mediaRequest">, Record @@ -18,6 +16,4 @@ export const mediaRequestStatsRequestHandler = createCachedIntegrationRequestHan users: await integrationInstance.getUsersAsync(), }; }, - cacheDuration: dayjs.duration(1, "minute"), - queryKey: "mediaRequestStats", }); diff --git a/packages/request-handler/src/media-server.ts b/packages/request-handler/src/media-server.ts index b13b90eb0..55a7850e0 100644 --- a/packages/request-handler/src/media-server.ts +++ b/packages/request-handler/src/media-server.ts @@ -1,12 +1,10 @@ -import dayjs from "dayjs"; - import type { IntegrationKindByCategory } from "@homarr/definitions"; import type { StreamSession } from "@homarr/integrations"; import { createIntegrationAsync } from "@homarr/integrations"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const mediaServerRequestHandler = createCachedIntegrationRequestHandler< +export const mediaServerRequestHandler = createIntegrationRequestHandler< StreamSession[], IntegrationKindByCategory<"mediaService">, { @@ -17,6 +15,4 @@ export const mediaServerRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getCurrentSessionsAsync({ showOnlyPlaying: input.showOnlyPlaying }); }, - cacheDuration: dayjs.duration(5, "seconds"), - queryKey: "mediaServerSessions", }); diff --git a/packages/request-handler/src/media-transcoding.ts b/packages/request-handler/src/media-transcoding.ts index 698d7347b..ba6455ccd 100644 --- a/packages/request-handler/src/media-transcoding.ts +++ b/packages/request-handler/src/media-transcoding.ts @@ -1,18 +1,14 @@ -import dayjs from "dayjs"; - import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { TdarrQueue, TdarrStatistics, TdarrWorker } from "@homarr/integrations"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const mediaTranscodingRequestHandler = createCachedIntegrationRequestHandler< +export const mediaTranscodingRequestHandler = createIntegrationRequestHandler< MediaTranscoding, IntegrationKindByCategory<"mediaTranscoding">, { pageOffset: number; pageSize: number } >({ - queryKey: "mediaTranscoding", - cacheDuration: dayjs.duration(5, "minutes"), async requestAsync(integration, input) { const integrationInstance = await createIntegrationAsync(integration); return { diff --git a/packages/request-handler/src/minecraft-server-status.ts b/packages/request-handler/src/minecraft-server-status.ts index 98d850171..664a0a7f3 100644 --- a/packages/request-handler/src/minecraft-server-status.ts +++ b/packages/request-handler/src/minecraft-server-status.ts @@ -1,14 +1,11 @@ -import dayjs from "dayjs"; import { z } from "zod/v4"; import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; -import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; +import { createWidgetRequestHandler } from "./lib/widget-request-handler"; -export const minecraftServerStatusRequestHandler = createCachedWidgetRequestHandler({ - queryKey: "minecraftServerStatusApiResult", - widgetKind: "minecraftServerStatus", +export const minecraftServerStatusRequestHandler = createWidgetRequestHandler({ async requestAsync(input: { domain: string; isBedrockServer: boolean }) { const path = `${input.isBedrockServer ? "/bedrock" : ""}/3/${input.domain}`; @@ -17,7 +14,6 @@ export const minecraftServerStatusRequestHandler = createCachedWidgetRequestHand ); return responseSchema.parse(await response.json()); }, - cacheDuration: dayjs.duration(5, "minutes"), }); const responseSchema = z diff --git a/packages/request-handler/src/navidrome.ts b/packages/request-handler/src/navidrome.ts index b59f852a8..0ad39ed9a 100644 --- a/packages/request-handler/src/navidrome.ts +++ b/packages/request-handler/src/navidrome.ts @@ -1,11 +1,9 @@ -import dayjs from "dayjs"; - import { createIntegrationAsync } from "@homarr/integrations"; import type { NavidromeDashboardData } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const navidromeRequestHandler = createCachedIntegrationRequestHandler< +export const navidromeRequestHandler = createIntegrationRequestHandler< NavidromeDashboardData, "navidrome", Record @@ -14,6 +12,4 @@ export const navidromeRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getDashboardDataAsync(); }, - cacheDuration: dayjs.duration(10, "minutes"), - queryKey: "navidromeDashboard", }); diff --git a/packages/request-handler/src/network-controller.ts b/packages/request-handler/src/network-controller.ts index f5975acf2..123261025 100644 --- a/packages/request-handler/src/network-controller.ts +++ b/packages/request-handler/src/network-controller.ts @@ -1,12 +1,10 @@ -import dayjs from "dayjs"; - import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { NetworkControllerSummary } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const networkControllerRequestHandler = createCachedIntegrationRequestHandler< +export const networkControllerRequestHandler = createIntegrationRequestHandler< NetworkControllerSummary, IntegrationKindByCategory<"networkController">, Record @@ -15,6 +13,4 @@ export const networkControllerRequestHandler = createCachedIntegrationRequestHan const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getNetworkSummaryAsync(); }, - cacheDuration: dayjs.duration(5, "minutes"), - queryKey: "networkControllerSummary", }); diff --git a/packages/request-handler/src/notifications.ts b/packages/request-handler/src/notifications.ts index 41098e660..479ff81fa 100644 --- a/packages/request-handler/src/notifications.ts +++ b/packages/request-handler/src/notifications.ts @@ -1,12 +1,10 @@ -import dayjs from "dayjs"; - import type { IntegrationKindByCategory } from "@homarr/definitions"; import type { Notification } from "@homarr/integrations"; import { createIntegrationAsync } from "@homarr/integrations"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const notificationsRequestHandler = createCachedIntegrationRequestHandler< +export const notificationsRequestHandler = createIntegrationRequestHandler< Notification[], IntegrationKindByCategory<"notifications">, Record @@ -15,6 +13,4 @@ export const notificationsRequestHandler = createCachedIntegrationRequestHandler const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getNotificationsAsync(); }, - cacheDuration: dayjs.duration(5, "minutes"), - queryKey: "notificationsJobStatus", }); diff --git a/packages/request-handler/src/paperless-ngx.ts b/packages/request-handler/src/paperless-ngx.ts index a3793658f..e6b088006 100644 --- a/packages/request-handler/src/paperless-ngx.ts +++ b/packages/request-handler/src/paperless-ngx.ts @@ -1,11 +1,9 @@ -import dayjs from "dayjs"; - import { createIntegrationAsync } from "@homarr/integrations"; import type { PaperlessNgxStats } from "@homarr/integrations"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const paperlessNgxStatsRequestHandler = createCachedIntegrationRequestHandler< +export const paperlessNgxStatsRequestHandler = createIntegrationRequestHandler< PaperlessNgxStats, "paperlessNgx", Record @@ -14,6 +12,4 @@ export const paperlessNgxStatsRequestHandler = createCachedIntegrationRequestHan const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getStatsAsync(); }, - cacheDuration: dayjs.duration(15, "minute"), - queryKey: "paperless-ngx-stats", }); diff --git a/packages/request-handler/src/patchmon.ts b/packages/request-handler/src/patchmon.ts new file mode 100644 index 000000000..021897511 --- /dev/null +++ b/packages/request-handler/src/patchmon.ts @@ -0,0 +1,15 @@ +import { createIntegrationAsync } from "@homarr/integrations"; +import type { PatchMonStats } from "@homarr/integrations"; + +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; + +export const patchmonStatsRequestHandler = createIntegrationRequestHandler< + PatchMonStats, + "patchmon", + Record +>({ + async requestAsync(integration) { + const integrationInstance = await createIntegrationAsync(integration); + return await integrationInstance.getStatsAsync(); + }, +}); diff --git a/packages/request-handler/src/release-providers.ts b/packages/request-handler/src/release-providers.ts new file mode 100644 index 000000000..d4a9dd2b3 --- /dev/null +++ b/packages/request-handler/src/release-providers.ts @@ -0,0 +1,829 @@ +import { Octokit, RequestError as OctokitRequestError } from "octokit"; +import { z } from "zod/v4"; + +import type { ReleaseProviderKind } from "@homarr/definitions"; +import { getReleaseProviderDefaultUrl, normalizeReleaseProviderIdentifier } from "@homarr/definitions"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; +import { createLogger } from "@homarr/core/infrastructure/logs"; + +const logger = createLogger({ module: "releaseProviders" }); + +const PROVIDER_TIMEOUT_MS = 10_000; +const fetchProvider: typeof fetchWithTrustedCertificatesAsync = (url, options) => + fetchWithTrustedCertificatesAsync(url, { timeout: PROVIDER_TIMEOUT_MS, ...options }); + +export interface ReleasesRepositoryRequest extends Record { + id: string; + provider: ReleaseProviderKind; + identifier: string; + versionRegex?: string; + providerUrl?: string; + token?: string; +} + +export interface DetailsProviderResponse { + projectUrl?: string; + projectDescription?: string; + isFork?: boolean; + isArchived?: boolean; + createdAt?: Date; + starsCount?: number; + openIssues?: number; + forksCount?: number; +} + +export interface ReleaseProviderResponse { + latestRelease: string; + latestReleaseAt: Date; + releaseUrl?: string; + releaseDescription?: string; + isPreRelease?: boolean; +} + +export type ReleaseData = DetailsProviderResponse & ReleaseProviderResponse; +export type ReleaseError = { + code: "invalidIdentifier" | "noMatchingVersion" | "noReleasesFound" | "unexpected"; + message: string; +}; +export type ReleaseResponse = { success: true; data: ReleaseData } | { success: false; error: ReleaseError }; + +const compileVersionRegex = (versionRegex?: string): RegExp | null => { + if (!versionRegex) return null; + try { + return new RegExp(versionRegex); + } catch { + return null; + } +}; + +export const getLatestRelease = ( + releases: ReleaseProviderResponse[], + versionRegex?: string, +): { release: ReleaseProviderResponse } | { noMatch: string } => { + const regex = compileVersionRegex(versionRegex); + if (versionRegex && !regex) { + return { noMatch: `Invalid regex: /${versionRegex}/` }; + } + + const validReleases = releases.filter((result) => { + if (!result.latestRelease) return false; + return regex ? regex.test(result.latestRelease) : true; + }); + + if (validReleases.length > 0) { + return { + release: validReleases.reduce((latest, current) => + current.latestReleaseAt > latest.latestReleaseAt ? current : latest, + ), + }; + } + + const sampleTags = releases + .slice(0, 5) + .map((r) => r.latestRelease) + .join(", "); + const reason = versionRegex + ? `Regex /${versionRegex}/ matched 0/${releases.length} tags. Sample tags: [${sampleTags}]` + : `Found ${releases.length} releases but none had a valid tag`; + return { noMatch: reason }; +}; + +export const getLatestMatchingReleaseAsync = async (input: ReleasesRepositoryRequest): Promise => { + const baseUrl = removeTrailingSlash(input.providerUrl ?? getReleaseProviderDefaultUrl(input.provider)); + const identifier = normalizeReleaseProviderIdentifier(input.provider, input.identifier); + + switch (input.provider) { + case "github": + return await getGithubReleaseAsync(baseUrl, identifier, input.versionRegex, input.token); + case "gitHubContainerRegistry": + return await getGitHubContainerRegistryReleaseAsync(baseUrl, identifier, input.versionRegex, input.token); + case "dockerHub": + return await getDockerHubReleaseAsync(baseUrl, identifier, input.versionRegex, input.token); + case "gitlab": + return await getGitlabReleaseAsync(baseUrl, identifier, input.versionRegex, input.token); + case "npm": + return await getNpmReleaseAsync(baseUrl, identifier, input.versionRegex, input.token); + case "codeberg": + return await getCodebergReleaseAsync(baseUrl, identifier, input.versionRegex, input.token); + case "linuxServerIO": + return await getLinuxServerIOReleaseAsync(baseUrl, identifier, input.versionRegex); + case "quay": + return await getQuayReleaseAsync(baseUrl, identifier, input.versionRegex); + } +}; + +const parseOwnerName = (identifier: string) => { + const [owner, name] = identifier.split("/"); + if (!owner || !name) return null; + return { owner, name }; +}; + +const parseOwnerPackageName = (identifier: string) => { + const [owner, ...nameParts] = identifier.split("/"); + const name = nameParts.join("/"); + if (!owner || !name) return null; + return { owner, name }; +}; + +const removeTrailingSlash = (url: string) => url.replace(/\/$/, ""); + +const buildUrl = (baseUrl: string, path: `/${string}`) => new URL(`${baseUrl}${path}`).toString(); + +const getGithubApi = (baseUrl: string, userAgent: string, token?: string) => + new Octokit({ + baseUrl, + auth: token, + request: { fetch: fetchWithTrustedCertificatesAsync }, + throttle: { enabled: true }, + userAgent, + }); + +const getGithubReleaseAsync = async ( + baseUrl: string, + identifier: string, + versionRegex?: string, + token?: string, +): Promise => { + const parsed = parseOwnerName(identifier); + if (!parsed) + return { + success: false, + error: { code: "invalidIdentifier", message: `Cannot parse "${identifier}" as owner/repo` }, + }; + + const api = getGithubApi(baseUrl, "Homarr-Lab/Homarr:GithubReleaseProvider", token); + try { + const allReleases = await api.paginate(api.rest.repos.listReleases, { + owner: parsed.owner, + repo: parsed.name, + per_page: 100, + }); + if (allReleases.length === 0) { + return { success: false, error: { code: "noReleasesFound", message: `${identifier} has no GitHub releases` } }; + } + + const releases = allReleases.reduce((acc, release) => { + if (!release.published_at) return acc; + acc.push({ + latestRelease: release.tag_name, + latestReleaseAt: new Date(release.published_at), + releaseUrl: release.html_url, + releaseDescription: release.body ?? undefined, + isPreRelease: release.prerelease, + }); + return acc; + }, []); + const result = getLatestRelease(releases, versionRegex); + if ("noMatch" in result) { + return { success: false, error: { code: "noMatchingVersion", message: result.noMatch } }; + } + + const details = await getGithubDetailsAsync(api, parsed.owner, parsed.name); + return { success: true, data: { ...details, ...result.release } }; + } catch (error) { + const message = error instanceof OctokitRequestError ? error.message : String(error); + logger.warn("Failed to get GitHub releases", { identifier, error: message }); + return { success: false, error: { code: "unexpected", message } }; + } +}; + +const getGithubDetailsAsync = async (api: Octokit, owner: string, name: string): Promise => { + try { + const response = await api.rest.repos.get({ owner, repo: name }); + return { + projectUrl: response.data.html_url, + projectDescription: response.data.description ?? undefined, + isFork: response.data.fork, + isArchived: response.data.archived, + createdAt: new Date(response.data.created_at), + starsCount: response.data.stargazers_count, + openIssues: response.data.open_issues_count, + forksCount: response.data.forks_count, + }; + } catch (error) { + logger.warn("Failed to get GitHub details", { owner, name, error: String(error) }); + return {}; + } +}; + +const getGitHubContainerRegistryReleaseAsync = async ( + baseUrl: string, + identifier: string, + versionRegex?: string, + token?: string, +): Promise => { + const parsed = parseOwnerPackageName(identifier); + if (!parsed) + return { + success: false, + error: { code: "invalidIdentifier", message: `Cannot parse "${identifier}" as owner/package` }, + }; + + try { + const repositoryName = `${parsed.owner}/${parsed.name}`; + const regex = compileVersionRegex(versionRegex); + if (versionRegex && !regex) { + return { success: false, error: { code: "noMatchingVersion", message: `Invalid regex: /${versionRegex}/` } }; + } + const registryUrl = new URL(baseUrl); + const registryToken = token ?? (await getGitHubContainerRegistryTokenAsync(registryUrl, repositoryName)); + const tags = await getGitHubContainerRegistryTagsAsync(registryUrl, repositoryName, registryToken); + if (tags.length === 0) { + return { success: false, error: { code: "noReleasesFound", message: `${repositoryName} has no container tags` } }; + } + const matchingTags = regex ? tags.filter((tag) => regex.test(tag)) : tags; + const tagsToCheck = versionRegex + ? matchingTags.slice(-100) + : matchingTags.includes("latest") + ? ["latest"] + : matchingTags.slice(-100); + if (tagsToCheck.length === 0) { + const sampleTags = tags.slice(0, 5).join(", "); + const msg = versionRegex + ? `Regex /${versionRegex}/ matched 0/${tags.length} tags. Sample: [${sampleTags}]` + : `No "latest" tag found among ${tags.length} tags. Sample: [${sampleTags}]`; + return { success: false, error: { code: "noMatchingVersion", message: msg } }; + } + + const releases = await Promise.all( + tagsToCheck.map(async (tag) => ({ + latestRelease: tag, + latestReleaseAt: await getGitHubContainerRegistryTagCreatedAtAsync( + registryUrl, + repositoryName, + tag, + registryToken, + ), + releaseUrl: `https://github.com/${repositoryName}/pkgs/container/${encodeURIComponent(parsed.name)}`, + })), + ); + const result = getLatestRelease(releases, versionRegex); + if ("noMatch" in result) { + return { success: false, error: { code: "noMatchingVersion", message: result.noMatch } }; + } + + return { + success: true, + data: { + projectUrl: `https://github.com/${repositoryName}`, + ...result.release, + }, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + logger.warn("Failed to get GitHub container registry releases", { identifier, error: message }); + return { success: false, error: { code: "unexpected", message } }; + } +}; + +const gitHubContainerRegistryTokenSchema = z.object({ token: z.string() }); +const gitHubContainerRegistryTagsSchema = z.object({ tags: z.array(z.string()).optional() }); +const gitHubContainerRegistryManifestSchema = z.object({ + config: z.object({ digest: z.string() }).optional(), + manifests: z.array(z.object({ digest: z.string() })).optional(), +}); +const gitHubContainerRegistryConfigSchema = z.object({ + created: z.string().transform((value) => new Date(value)), +}); + +const getGitHubContainerRegistryTokenAsync = async (registryUrl: URL, repositoryName: string) => { + const response = await fetchProvider( + buildUrl( + registryUrl.origin, + `/token?service=${registryUrl.host}&scope=${encodeURIComponent(`repository:${repositoryName}:pull`)}` as `/${string}`, + ), + ); + if (!response.ok) throw new Error(response.statusText); + + const result = gitHubContainerRegistryTokenSchema.safeParse(await response.json()); + if (!result.success) throw new Error(result.error.message); + + return result.data.token; +}; + +const getGitHubContainerRegistryTagsAsync = async (registryUrl: URL, repositoryName: string, token: string) => { + const response = await fetchProvider(buildUrl(registryUrl.origin, `/v2/${repositoryName}/tags/list`), { + headers: { Authorization: `Bearer ${token}` }, + }); + if (!response.ok) throw new Error(response.statusText); + + const result = gitHubContainerRegistryTagsSchema.safeParse(await response.json()); + if (!result.success) throw new Error(result.error.message); + + return result.data.tags ?? []; +}; + +const getGitHubContainerRegistryTagCreatedAtAsync = async ( + registryUrl: URL, + repositoryName: string, + tag: string, + token: string, +) => { + const manifest = await getGitHubContainerRegistryManifestAsync(registryUrl, repositoryName, tag, token); + const imageManifest = manifest.config + ? manifest + : await getGitHubContainerRegistryManifestAsync( + registryUrl, + repositoryName, + manifest.manifests?.[0]?.digest ?? "", + token, + ); + const configDigest = imageManifest.config?.digest; + if (!configDigest) throw new Error("Missing image config digest"); + + const response = await fetchProvider(buildUrl(registryUrl.origin, `/v2/${repositoryName}/blobs/${configDigest}`), { + headers: { Authorization: `Bearer ${token}` }, + }); + if (!response.ok) throw new Error(response.statusText); + + const result = gitHubContainerRegistryConfigSchema.safeParse(await response.json()); + if (!result.success) throw new Error(result.error.message); + + return result.data.created; +}; + +const getGitHubContainerRegistryManifestAsync = async ( + registryUrl: URL, + repositoryName: string, + reference: string, + token: string, +) => { + if (!reference) throw new Error("Missing image manifest reference"); + + const response = await fetchProvider(buildUrl(registryUrl.origin, `/v2/${repositoryName}/manifests/${reference}`), { + headers: { + Authorization: `Bearer ${token}`, + Accept: [ + "application/vnd.oci.image.index.v1+json", + "application/vnd.docker.distribution.manifest.list.v2+json", + "application/vnd.oci.image.manifest.v1+json", + "application/vnd.docker.distribution.manifest.v2+json", + ].join(", "), + }, + }); + if (!response.ok) throw new Error(response.statusText); + + const result = gitHubContainerRegistryManifestSchema.safeParse(await response.json()); + if (!result.success) throw new Error(result.error.message); + + return result.data; +}; + +const dockerHubReleasesSchema = z.object({ + results: z.array( + z.object({ name: z.string(), last_updated: z.string().transform((value) => new Date(value)) }).transform((tag) => ({ + latestRelease: tag.name, + latestReleaseAt: tag.last_updated, + })), + ), +}); + +const dockerHubDetailsSchema = z.object({ + name: z.string(), + namespace: z.string(), + description: z.string(), + star_count: z.number(), + date_registered: z.string().transform((value) => new Date(value)), +}); + +const getDockerHubReleaseAsync = async ( + baseUrl: string, + identifier: string, + versionRegex?: string, + token?: string, +): Promise => { + const parsed = identifier.includes("/") ? parseOwnerName(identifier) : { owner: "", name: identifier }; + if (!parsed) return { success: false, error: { code: "invalidIdentifier", message: `Cannot parse "${identifier}"` } }; + + const relativeUrl = parsed.owner + ? `/v2/namespaces/${encodeURIComponent(parsed.owner)}/repositories/${encodeURIComponent(parsed.name)}` + : `/v2/repositories/library/${encodeURIComponent(parsed.name)}`; + + const headers: Record = {}; + if (token) headers.Authorization = `Bearer ${token}`; + + let lastNoMatch = ""; + for (let page = 0; page <= 5; page++) { + const response = await fetchProvider( + buildUrl(baseUrl, `${relativeUrl}/tags?page_size=100&page=${page}` as `/${string}`), + headers.Authorization ? { headers } : undefined, + ); + if (!response.ok) + return { + success: false, + error: { code: "unexpected", message: `Docker Hub ${response.status}: ${response.statusText}` }, + }; + + const json: unknown = await response.json(); + const result = dockerHubReleasesSchema.safeParse(json); + if (!result.success) return { success: false, error: { code: "unexpected", message: result.error.message } }; + + const matched = getLatestRelease(result.data.results, versionRegex); + if ("noMatch" in matched) { + lastNoMatch = matched.noMatch; + continue; + } + + const details = await getDockerHubDetailsAsync(baseUrl, relativeUrl as `/${string}`, token); + return { success: true, data: { ...details, ...matched.release } }; + } + + return { + success: false, + error: { code: "noMatchingVersion", message: lastNoMatch || `No matching tags found for ${identifier}` }, + }; +}; + +const getDockerHubDetailsAsync = async ( + baseUrl: string, + relativeUrl: `/${string}`, + token?: string, +): Promise => { + const headers: Record = {}; + if (token) headers.Authorization = `Bearer ${token}`; + + const response = await fetchProvider( + buildUrl(baseUrl, `${relativeUrl}/` as `/${string}`), + headers.Authorization ? { headers } : undefined, + ); + if (!response.ok) return {}; + + const result = dockerHubDetailsSchema.safeParse(await response.json()); + if (!result.success) return {}; + + return { + projectUrl: `https://hub.docker.com/r/${result.data.namespace}/${result.data.name}`, + projectDescription: result.data.description, + createdAt: result.data.date_registered, + starsCount: result.data.star_count, + }; +}; + +const gitlabReleasesSchema = z.array( + z.object({ + name: z.string().nullable().optional(), + tag_name: z.string(), + released_at: z.string().nullable().optional(), + description: z.string().nullable().optional(), + _links: z.object({ self: z.string().optional() }).optional(), + }), +); + +const gitlabDetailsSchema = z.object({ + web_url: z.string().optional(), + description: z.string().nullable().optional(), + forked_from_project: z.unknown().nullable().optional(), + archived: z.boolean(), + created_at: z.string(), + star_count: z.number(), + open_issues_count: z.number().optional(), + forks_count: z.number(), +}); + +const getGitlabReleaseAsync = async ( + baseUrl: string, + identifier: string, + versionRegex?: string, + token?: string, +): Promise => { + const encodedIdentifier = encodeURIComponent(identifier); + const headers: Record = {}; + if (token) headers["PRIVATE-TOKEN"] = token; + + const response = await fetchProvider( + buildUrl(baseUrl, `/api/v4/projects/${encodedIdentifier}/releases?per_page=100`), + headers["PRIVATE-TOKEN"] ? { headers } : undefined, + ); + if (!response.ok) + return { + success: false, + error: { code: "unexpected", message: `GitLab ${response.status}: ${response.statusText}` }, + }; + + const result = gitlabReleasesSchema.safeParse(await response.json()); + if (!result.success) return { success: false, error: { code: "unexpected", message: result.error.message } }; + if (result.data.length === 0) + return { success: false, error: { code: "noReleasesFound", message: `${identifier} has no GitLab releases` } }; + + const releases = result.data.reduce((acc, release) => { + if (!release.released_at) return acc; + const releaseDate = new Date(release.released_at); + acc.push({ + latestRelease: release.tag_name, + latestReleaseAt: releaseDate, + releaseUrl: release._links?.self, + releaseDescription: release.description ?? undefined, + isPreRelease: releaseDate > new Date(), + }); + return acc; + }, []); + const matched = getLatestRelease(releases, versionRegex); + if ("noMatch" in matched) return { success: false, error: { code: "noMatchingVersion", message: matched.noMatch } }; + + const details = await getGitlabDetailsAsync(baseUrl, encodedIdentifier, token); + return { success: true, data: { ...details, ...matched.release } }; +}; + +const getGitlabDetailsAsync = async ( + baseUrl: string, + encodedIdentifier: string, + token?: string, +): Promise => { + const headers: Record = {}; + if (token) headers["PRIVATE-TOKEN"] = token; + + const response = await fetchProvider( + buildUrl(baseUrl, `/api/v4/projects/${encodedIdentifier}`), + headers["PRIVATE-TOKEN"] ? { headers } : undefined, + ); + if (!response.ok) return {}; + + const result = gitlabDetailsSchema.safeParse(await response.json()); + if (!result.success || !result.data.web_url) return {}; + + return { + projectUrl: result.data.web_url, + projectDescription: result.data.description ?? undefined, + isFork: result.data.forked_from_project !== null, + isArchived: result.data.archived, + createdAt: new Date(result.data.created_at), + starsCount: result.data.star_count, + openIssues: result.data.open_issues_count, + forksCount: result.data.forks_count, + }; +}; + +const npmTimeMetadataKeys = new Set(["created", "modified"]); + +const npmReleasesSchema = z.object({ + time: z + .record( + z.string(), + z.string().transform((value) => new Date(value)), + ) + .transform((entries) => + Object.entries(entries) + .filter(([key]) => !npmTimeMetadataKeys.has(key)) + .map(([key, value]) => ({ latestRelease: key, latestReleaseAt: value })), + ), + versions: z.record(z.string(), z.object({ description: z.string().optional() })), + name: z.string(), +}); + +const getNpmReleaseAsync = async ( + baseUrl: string, + identifier: string, + versionRegex?: string, + token?: string, +): Promise => { + if (!identifier) return { success: false, error: { code: "invalidIdentifier", message: "Empty npm package name" } }; + + const headers: Record = {}; + if (token) headers.Authorization = `Bearer ${token}`; + + const response = await fetchProvider( + buildUrl(baseUrl, `/${encodeURIComponent(identifier)}`), + headers.Authorization ? { headers } : undefined, + ); + if (!response.ok) + return { success: false, error: { code: "unexpected", message: `npm ${response.status}: ${response.statusText}` } }; + + const result = npmReleasesSchema.safeParse(await response.json()); + if (!result.success) return { success: false, error: { code: "unexpected", message: result.error.message } }; + + const releases = result.data.time.map((tag) => ({ + ...tag, + releaseUrl: `https://www.npmjs.com/package/${encodeURIComponent(result.data.name)}/v/${encodeURIComponent(tag.latestRelease)}`, + releaseDescription: result.data.versions[tag.latestRelease]?.description ?? "", + })); + const matched = getLatestRelease(releases, versionRegex); + if ("noMatch" in matched) return { success: false, error: { code: "noMatchingVersion", message: matched.noMatch } }; + + return { success: true, data: matched.release }; +}; + +const codebergReleasesSchema = z.array( + z.object({ + tag_name: z.string(), + published_at: z.string().transform((value) => new Date(value)), + url: z.string(), + body: z.string(), + prerelease: z.boolean(), + }), +); + +const codebergDetailsSchema = z.object({ + html_url: z.string(), + description: z.string(), + fork: z.boolean(), + archived: z.boolean(), + created_at: z.string().transform((value) => new Date(value)), + stars_count: z.number(), + open_issues_count: z.number(), + forks_count: z.number(), +}); + +const getCodebergReleaseAsync = async ( + baseUrl: string, + identifier: string, + versionRegex?: string, + token?: string, +): Promise => { + const parsed = parseOwnerName(identifier); + if (!parsed) + return { + success: false, + error: { code: "invalidIdentifier", message: `Cannot parse "${identifier}" as owner/repo` }, + }; + + const headers: Record = {}; + if (token) headers.Authorization = `token ${token}`; + + const response = await fetchProvider( + buildUrl(baseUrl, `/api/v1/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.name)}/releases`), + headers.Authorization ? { headers } : undefined, + ); + if (!response.ok) + return { + success: false, + error: { code: "unexpected", message: `Codeberg ${response.status}: ${response.statusText}` }, + }; + + const result = codebergReleasesSchema.safeParse(await response.json()); + if (!result.success) return { success: false, error: { code: "unexpected", message: result.error.message } }; + + const releases = result.data.map((tag) => ({ + latestRelease: tag.tag_name, + latestReleaseAt: tag.published_at, + releaseUrl: tag.url, + releaseDescription: tag.body, + isPreRelease: tag.prerelease, + })); + const matched = getLatestRelease(releases, versionRegex); + if ("noMatch" in matched) return { success: false, error: { code: "noMatchingVersion", message: matched.noMatch } }; + + const details = await getCodebergDetailsAsync(baseUrl, parsed.owner, parsed.name, token); + return { success: true, data: { ...details, ...matched.release } }; +}; + +const getCodebergDetailsAsync = async ( + baseUrl: string, + owner: string, + name: string, + token?: string, +): Promise => { + const headers: Record = {}; + if (token) headers.Authorization = `token ${token}`; + + const response = await fetchProvider( + buildUrl(baseUrl, `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`), + headers.Authorization ? { headers } : undefined, + ); + if (!response.ok) return {}; + + const result = codebergDetailsSchema.safeParse(await response.json()); + if (!result.success) return {}; + + return { + projectUrl: result.data.html_url, + projectDescription: result.data.description, + isFork: result.data.fork, + isArchived: result.data.archived, + createdAt: result.data.created_at, + starsCount: result.data.stars_count, + openIssues: result.data.open_issues_count, + forksCount: result.data.forks_count, + }; +}; + +const linuxServerIOReleasesSchema = z.object({ + data: z.object({ + repositories: z.object({ + linuxserver: z.array( + z.object({ + name: z.string(), + initial_date: z + .string() + .transform((value) => new Date(value)) + .optional(), + github_url: z.string(), + description: z.string(), + version: z.string(), + version_timestamp: z.string().transform((value) => new Date(value)), + stars: z.number(), + deprecated: z.boolean(), + changelog: z.array(z.object({ date: z.string(), desc: z.string() })).optional(), + }), + ), + }), + }), +}); + +const getLinuxServerIOReleaseAsync = async ( + baseUrl: string, + identifier: string, + versionRegex?: string, +): Promise => { + const parsed = parseOwnerName(identifier); + if (!parsed) + return { + success: false, + error: { code: "invalidIdentifier", message: `Cannot parse "${identifier}" as owner/name` }, + }; + + const response = await fetchProvider(buildUrl(baseUrl, "/api/v1/images")); + if (!response.ok) + return { + success: false, + error: { code: "unexpected", message: `LSIO ${response.status}: ${response.statusText}` }, + }; + + const result = linuxServerIOReleasesSchema.safeParse(await response.json()); + if (!result.success) return { success: false, error: { code: "unexpected", message: result.error.message } }; + + const release = result.data.data.repositories.linuxserver.find((repo) => repo.name === parsed.name); + if (!release) + return { + success: false, + error: { code: "noReleasesFound", message: `Image "${parsed.name}" not found in LinuxServer.io registry` }, + }; + + const regex = compileVersionRegex(versionRegex); + if (versionRegex && !regex) { + return { + success: false, + error: { code: "noMatchingVersion", message: `Invalid regex: /${versionRegex}/` }, + }; + } + + if (regex && !regex.test(release.version)) { + return { + success: false, + error: { + code: "noMatchingVersion", + message: `Regex /${versionRegex}/ does not match LSIO version "${release.version}"`, + }, + }; + } + + return { + success: true, + data: { + latestRelease: release.version, + latestReleaseAt: release.version_timestamp, + releaseDescription: release.changelog?.shift()?.desc, + projectUrl: release.github_url, + projectDescription: release.description, + isArchived: release.deprecated, + createdAt: release.initial_date, + starsCount: release.stars, + }, + }; +}; + +const quayReleasesSchema = z.object({ + description: z.string().optional(), + tags: z.record(z.string(), z.object({ name: z.string(), last_modified: z.string() })), +}); + +const getQuayReleaseAsync = async ( + baseUrl: string, + identifier: string, + versionRegex?: string, +): Promise => { + const parsed = parseOwnerName(identifier); + if (!parsed) + return { + success: false, + error: { code: "invalidIdentifier", message: `Cannot parse "${identifier}" as owner/repo` }, + }; + + const response = await fetchProvider( + buildUrl( + baseUrl, + `/api/v1/repository/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.name)}?includeTags=true&includeStats=true`, + ), + ); + if (!response.ok) + return { + success: false, + error: { code: "unexpected", message: `Quay ${response.status}: ${response.statusText}` }, + }; + + const result = quayReleasesSchema.safeParse(await response.json()); + if (!result.success) return { success: false, error: { code: "unexpected", message: result.error.message } }; + + const releases = Object.values(result.data.tags).reduce((acc, tag) => { + if (!tag.name || !tag.last_modified) return acc; + acc.push({ + latestRelease: tag.name, + latestReleaseAt: new Date(tag.last_modified), + releaseUrl: `https://quay.io/repository/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.name)}/tag/${encodeURIComponent(tag.name)}`, + }); + return acc; + }, []); + const matched = getLatestRelease(releases, versionRegex); + if ("noMatch" in matched) return { success: false, error: { code: "noMatchingVersion", message: matched.noMatch } }; + + return { success: true, data: { projectDescription: result.data.description, ...matched.release } }; +}; diff --git a/packages/request-handler/src/releases.ts b/packages/request-handler/src/releases.ts index 7bfd29348..e089bc661 100644 --- a/packages/request-handler/src/releases.ts +++ b/packages/request-handler/src/releases.ts @@ -1,20 +1,10 @@ -import dayjs from "dayjs"; +import type { ReleaseResponse, ReleasesRepositoryRequest } from "./release-providers"; +import { getLatestMatchingReleaseAsync } from "./release-providers"; +import { createRequestHandler } from "./lib/request-handler"; -import type { IntegrationKindByCategory } from "@homarr/definitions"; -import type { ReleaseResponse, ReleasesRepository } from "@homarr/integrations"; -import { createIntegrationAsync } from "@homarr/integrations"; - -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; - -export const releasesRequestHandler = createCachedIntegrationRequestHandler< - ReleaseResponse, - IntegrationKindByCategory<"releasesProvider">, - ReleasesRepository ->({ - requestAsync: async (integration, input) => { - const instance = await createIntegrationAsync(integration); - return instance.getLatestMatchingReleaseAsync(input.identifier, input.versionRegex); +export const releasesRequestHandler = createRequestHandler({ + async requestAsync(input) { + return await getLatestMatchingReleaseAsync(input); }, - cacheDuration: dayjs.duration(5, "minutes"), - queryKey: "repositoriesReleases", + cacheTtlMs: 5 * 60 * 1000, }); diff --git a/packages/request-handler/src/rss-feeds.ts b/packages/request-handler/src/rss-feeds.ts index 9eaf026ea..0597a325f 100644 --- a/packages/request-handler/src/rss-feeds.ts +++ b/packages/request-handler/src/rss-feeds.ts @@ -1,18 +1,15 @@ import type { FeedData, FeedEntry } from "@extractus/feed-extractor"; import { extract } from "@extractus/feed-extractor"; -import dayjs from "dayjs"; import { z } from "zod/v4"; import type { Modify } from "@homarr/common/types"; import { createLogger } from "@homarr/core/infrastructure/logs"; -import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; +import { createWidgetRequestHandler } from "./lib/widget-request-handler"; const logger = createLogger({ module: "rssFeedsRequestHandler" }); -export const rssFeedsRequestHandler = createCachedWidgetRequestHandler({ - queryKey: "rssFeedList", - widgetKind: "rssFeed", +export const rssFeedsRequestHandler = createWidgetRequestHandler({ async requestAsync(input: { url: string; count: number }) { const result = (await extract(input.url, { getExtraEntryFields: (feedEntry) => { @@ -31,7 +28,6 @@ export const rssFeedsRequestHandler = createCachedWidgetRequestHandler({ entries: result.entries?.map((entry) => ({ ...entry, feedUrl: input.url })).slice(0, input.count) ?? [], }; }, - cacheDuration: dayjs.duration(5, "minutes"), }); const attemptGetImageFromEntry = (feedUrl: string, entry: object) => { diff --git a/packages/request-handler/src/smart-home-entity-state.ts b/packages/request-handler/src/smart-home-entity-state.ts index 5fe02b2ac..3c8851522 100644 --- a/packages/request-handler/src/smart-home-entity-state.ts +++ b/packages/request-handler/src/smart-home-entity-state.ts @@ -1,11 +1,9 @@ -import dayjs from "dayjs"; - import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const smartHomeEntityStateRequestHandler = createCachedIntegrationRequestHandler< +export const smartHomeEntityStateRequestHandler = createIntegrationRequestHandler< string, IntegrationKindByCategory<"smartHomeServer">, { entityId: string } @@ -20,6 +18,4 @@ export const smartHomeEntityStateRequestHandler = createCachedIntegrationRequest return result.data.state; }, - cacheDuration: dayjs.duration(1, "minute"), - queryKey: "smartHome-entityState", }); diff --git a/packages/request-handler/src/speedtest-tracker.ts b/packages/request-handler/src/speedtest-tracker.ts index 799d37f16..3f03f7187 100644 --- a/packages/request-handler/src/speedtest-tracker.ts +++ b/packages/request-handler/src/speedtest-tracker.ts @@ -1,11 +1,9 @@ -import dayjs from "dayjs"; - import { createIntegrationAsync } from "@homarr/integrations"; import type { SpeedtestTrackerDashboardData } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const speedtestTrackerRequestHandler = createCachedIntegrationRequestHandler< +export const speedtestTrackerRequestHandler = createIntegrationRequestHandler< SpeedtestTrackerDashboardData, "speedtestTracker", Record @@ -14,6 +12,4 @@ export const speedtestTrackerRequestHandler = createCachedIntegrationRequestHand const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getDashboardDataAsync(); }, - cacheDuration: dayjs.duration(10, "minutes"), - queryKey: "speedtestTrackerDashboard", }); diff --git a/packages/request-handler/src/stock-price.ts b/packages/request-handler/src/stock-price.ts index b0ca15e47..177b223aa 100644 --- a/packages/request-handler/src/stock-price.ts +++ b/packages/request-handler/src/stock-price.ts @@ -1,14 +1,11 @@ -import dayjs from "dayjs"; import { z } from "zod/v4"; import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; -import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; +import { createWidgetRequestHandler } from "./lib/widget-request-handler"; -export const fetchStockPriceHandler = createCachedWidgetRequestHandler({ - queryKey: "fetchStockPriceResult", - widgetKind: "stockPrice", +export const fetchStockPriceHandler = createWidgetRequestHandler({ async requestAsync(input: { stock: string; timeRange: string; timeInterval: string }) { const response = await withTimeoutAsync(async (signal) => { return await fetchWithTrustedCertificatesAsync( @@ -42,7 +39,6 @@ export const fetchStockPriceHandler = createCachedWidgetRequestHandler({ shortName: firstResult.meta.shortName, }; }, - cacheDuration: dayjs.duration(5, "minutes"), }); const dataSchema = z diff --git a/packages/request-handler/src/test/docker.spec.ts b/packages/request-handler/src/test/docker.spec.ts index b4e34f816..12dbb347b 100644 --- a/packages/request-handler/src/test/docker.spec.ts +++ b/packages/request-handler/src/test/docker.spec.ts @@ -75,6 +75,26 @@ describe("calculateCpuUsage", () => { // (500 / 100000) * 2 * 100 = 1 expect(calculateCpuUsage(stats)).toBe(1); }); + + test("should use the delta against precpu_stats for the current usage", () => { + const stats = createStats({ + cpu_stats: { online_cpus: 4, cpu_usage: { total_usage: 1_002_000 }, system_cpu_usage: 20_000 }, + precpu_stats: { cpu_usage: { total_usage: 1_000_000 }, system_cpu_usage: 10_000 }, + }); + // Despite a large lifetime total_usage, only the recent delta counts: + // (2000 / 10000) * 4 * 100 = 80 + expect(calculateCpuUsage(stats)).toBe(80); + }); + + test("should report 0 for a container that was busy over its lifetime but is idle now", () => { + const stats = createStats({ + cpu_stats: { online_cpus: 4, cpu_usage: { total_usage: 1_000_000 }, system_cpu_usage: 20_000 }, + precpu_stats: { cpu_usage: { total_usage: 1_000_000 }, system_cpu_usage: 10_000 }, + }); + // No CPU delta since the last reading -> current usage is 0, even though the + // lifetime average would be high. This is the regression the fix addresses. + expect(calculateCpuUsage(stats)).toBe(0); + }); }); describe("calculateMemoryUsage", () => { diff --git a/packages/request-handler/src/test/release-providers.spec.ts b/packages/request-handler/src/test/release-providers.spec.ts new file mode 100644 index 000000000..67ed3d71c --- /dev/null +++ b/packages/request-handler/src/test/release-providers.spec.ts @@ -0,0 +1,192 @@ +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { Response } from "undici"; + +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; + +import { getLatestMatchingReleaseAsync } from "../release-providers"; + +vi.mock("@homarr/core/infrastructure/http", () => ({ + fetchWithTrustedCertificatesAsync: vi.fn(), +})); +vi.mock("@homarr/core/infrastructure/logs", () => ({ + createLogger: () => ({ + warn: vi.fn(), + }), +})); + +const mockedFetch = vi.mocked(fetchWithTrustedCertificatesAsync); + +describe("getLatestMatchingReleaseAsync for GitHub Container Registry", () => { + beforeEach(() => { + mockedFetch.mockReset(); + }); + + test("normalizes full image refs and returns latest from anonymous OCI registry metadata", async () => { + mockGhcrResponses({ + repositoryName: "homarr-labs/homarr", + tags: ["v1.0.0", "latest"], + createdAtByTag: { + latest: "2026-06-19T20:30:59.853Z", + }, + }); + + const result = await getLatestMatchingReleaseAsync({ + id: "homarr", + provider: "gitHubContainerRegistry", + identifier: "ghcr.io/homarr-labs/homarr:latest", + }); + + expect(result).toEqual({ + success: true, + data: { + projectUrl: "https://github.com/homarr-labs/homarr", + latestRelease: "latest", + latestReleaseAt: new Date("2026-06-19T20:30:59.853Z"), + releaseUrl: "https://github.com/homarr-labs/homarr/pkgs/container/homarr", + }, + }); + expectUrl(0, "https://ghcr.io/token"); + expect(new URL(mockedFetch.mock.calls[0]?.[0].toString() ?? "").searchParams.get("service")).toBe("ghcr.io"); + expect(new URL(mockedFetch.mock.calls[0]?.[0].toString() ?? "").searchParams.get("scope")).toBe( + "repository:homarr-labs/homarr:pull", + ); + }); + + test("uses custom registry base URL for GHCR-compatible registries", async () => { + mockGhcrResponses({ + registryOrigin: "https://registry.example.com", + repositoryName: "homarr-labs/homarr", + tags: ["latest"], + createdAtByTag: { + latest: "2026-06-19T20:30:59.853Z", + }, + }); + + await getLatestMatchingReleaseAsync({ + id: "homarr", + provider: "gitHubContainerRegistry", + identifier: "homarr-labs/homarr", + providerUrl: "https://registry.example.com/", + }); + + expectUrl(0, "https://registry.example.com/token"); + expect(new URL(mockedFetch.mock.calls[0]?.[0].toString() ?? "").searchParams.get("service")).toBe( + "registry.example.com", + ); + expectUrl(1, "https://registry.example.com/v2/homarr-labs/homarr/tags/list"); + }); + + test("chooses the newest matching version tag", async () => { + mockGhcrResponses({ + repositoryName: "muchobien/pocketbase", + tags: ["0.22.0", "0.23.0", "latest"], + createdAtByTag: { + "0.22.0": "2026-01-01T00:00:00.000Z", + "0.23.0": "2026-02-01T00:00:00.000Z", + }, + }); + + const result = await getLatestMatchingReleaseAsync({ + id: "pocketbase", + provider: "gitHubContainerRegistry", + identifier: "ghcr.io/muchobien/pocketbase:latest", + versionRegex: "^0\\.[0-9]+\\.[0-9]+$", + }); + + expect(result.success).toBe(true); + if (!result.success) return; + expect(result.data.latestRelease).toBe("0.23.0"); + expect(result.data.latestReleaseAt).toEqual(new Date("2026-02-01T00:00:00.000Z")); + }); + + test("returns invalidIdentifier for missing package name", async () => { + const result = await getLatestMatchingReleaseAsync({ + id: "invalid", + provider: "gitHubContainerRegistry", + identifier: "ghcr.io/homarr-labs", + }); + + expect(result).toEqual({ + success: false, + error: expect.objectContaining({ code: "invalidIdentifier" }), + }); + expect(mockedFetch).not.toHaveBeenCalled(); + }); + + test("returns unexpected with provider error message", async () => { + mockedFetch.mockResolvedValueOnce(createJsonResponse({ errors: [] }, { status: 500, statusText: "Registry down" })); + + const result = await getLatestMatchingReleaseAsync({ + id: "homarr", + provider: "gitHubContainerRegistry", + identifier: "homarr-labs/homarr", + }); + + expect(result).toEqual({ success: false, error: { code: "unexpected", message: "Registry down" } }); + }); +}); + +interface MockGhcrResponsesInput { + registryOrigin?: string; + repositoryName: string; + tags: string[]; + createdAtByTag: Record; +} + +const mockGhcrResponses = ({ + registryOrigin = "https://ghcr.io", + repositoryName, + tags, + createdAtByTag, +}: MockGhcrResponsesInput) => { + mockedFetch.mockImplementation(async (input) => { + const url = new URL(input.toString()); + const path = url.pathname; + + if (url.origin !== registryOrigin) { + return createJsonResponse({ error: "unexpected origin" }, { status: 404, statusText: "Not Found" }); + } + + if (path === "/token") return createJsonResponse({ token: "token" }); + if (path === `/v2/${repositoryName}/tags/list`) return createJsonResponse({ name: repositoryName, tags }); + + const manifestMatch = new RegExp(`^/v2/${escapeRegExp(repositoryName)}/manifests/(.+)$`).exec(path); + if (manifestMatch) { + const reference = decodeURIComponent(manifestMatch[1] ?? ""); + if (reference.startsWith("sha256:")) { + return createJsonResponse({ config: { digest: `sha256:config-${reference.slice("sha256:".length)}` } }); + } + return createJsonResponse({ + manifests: [{ digest: `sha256:${reference}` }], + }); + } + + const blobMatch = new RegExp(`^/v2/${escapeRegExp(repositoryName)}/blobs/sha256:config-(.+)$`).exec(path); + if (blobMatch) { + const tag = decodeURIComponent(blobMatch[1] ?? ""); + return createJsonResponse({ created: createdAtByTag[tag] ?? "2026-01-01T00:00:00.000Z" }); + } + + return createJsonResponse({ error: "not found" }, { status: 404, statusText: "Not Found" }); + }); +}; + +interface JsonResponseInit { + status?: number; + statusText?: string; +} + +const createJsonResponse = (body: unknown, init: JsonResponseInit = {}) => + new Response(JSON.stringify(body), { + status: init.status ?? 200, + statusText: init.statusText, + headers: { + "content-type": "application/json", + }, + }); + +const expectUrl = (callIndex: number, expectedUrl: string) => { + expect(mockedFetch.mock.calls[callIndex]?.[0].toString().split("?")[0]).toBe(expectedUrl); +}; + +const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); diff --git a/packages/request-handler/src/timetable.ts b/packages/request-handler/src/timetable.ts index 0946ac67d..2be7355d8 100644 --- a/packages/request-handler/src/timetable.ts +++ b/packages/request-handler/src/timetable.ts @@ -1,36 +1,154 @@ -import dayjs from "dayjs"; +import { z } from "zod/v4"; -import type { IntegrationKindByCategory } from "@homarr/definitions"; -import { createIntegrationAsync } from "@homarr/integrations"; -import type { Station, Timetable } from "@homarr/integrations/types"; +import { ResponseError } from "@homarr/common/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createRequestHandler } from "./lib/request-handler"; -export const timetableSearchStationsRequestHandler = createCachedIntegrationRequestHandler< +export interface Station { + id: string; + name: string; +} + +export interface TimetableEntry { + timestamp: Date; + delay: number; + line: { + name: string; + color: string | null; + } | null; + location: string; + platform: { + name: string; + hasChanged: boolean; + } | null; +} + +export interface Timetable { + stationId: string; + timestamp: Date; + entries: TimetableEntry[]; +} + +const supportedStationTypes = ["bus", "tram", "train", "ship", "cablecar", "funicular", "chairlift"]; + +export const timetableSearchStationsRequestHandler = createRequestHandler< Station[], - IntegrationKindByCategory<"timetable">, - { query: string } + { baseUrl: string; query: string } >({ - async requestAsync(integration, input) { - const integrationInstance = await createIntegrationAsync(integration); - return await integrationInstance.searchStationsAsync(input.query); + async requestAsync(input) { + return await searchStationsAsync(input.baseUrl, input.query); }, - queryKey: "timetableSearchStations", - cacheDuration: dayjs.duration(1, "day"), + cacheTtlMs: 24 * 60 * 60 * 1000, }); -export const timetableGetTimetableRequestHandler = createCachedIntegrationRequestHandler< +export const timetableGetTimetableRequestHandler = createRequestHandler< Timetable, - IntegrationKindByCategory<"timetable">, - { stationId: string; limit: number } + { baseUrl: string; stationId: string; limit: number } >({ - async requestAsync(integration, input) { - const integrationInstance = await createIntegrationAsync(integration); - return await integrationInstance.getTimetableAsync({ - stationId: input.stationId, - limit: input.limit, - }); + async requestAsync(input) { + return await getTimetableAsync(input.baseUrl, { stationId: input.stationId, limit: input.limit }); }, - queryKey: "timetableGetTimetable", - cacheDuration: dayjs.duration(1, "minute"), + cacheTtlMs: 60_000, }); + +const buildUrl = (baseUrl: string, path: `/${string}`, queryParams: Record) => { + const url = new URL(`${baseUrl.replace(/\/$/, "")}${path}`); + for (const [key, value] of Object.entries(queryParams)) { + url.searchParams.set(key, value.toString()); + } + return url.toString(); +}; + +const searchStationsAsync = async (baseUrl: string, query: string): Promise => { + const response = await fetchWithTrustedCertificatesAsync( + buildUrl(baseUrl, "/timetable/api/completion.json", { term: query, show_ids: 1, nofavorites: 1 }), + ); + if (!response.ok) throw new ResponseError(response); + + const data = await searchSchema.parseAsync(await response.json()); + return data + .filter((item) => supportedStationTypes.some((type) => item.iconclass.endsWith(type))) + .map((item) => (item.id !== undefined ? { id: item.id, name: item.label } : null)) + .filter((item) => item !== null); +}; + +const getTimetableAsync = async ( + baseUrl: string, + options: { stationId: string; limit: number }, +): Promise => { + const now = new Date(); + const date = new Intl.DateTimeFormat("en-US", { + timeZone: "Europe/Zurich", + month: "2-digit", + day: "2-digit", + year: "numeric", + }).format(now); + const time = new Intl.DateTimeFormat("en-GB", { + timeZone: "Europe/Zurich", + hour: "2-digit", + minute: "2-digit", + hour12: false, + }).format(now); + const response = await fetchWithTrustedCertificatesAsync( + buildUrl(baseUrl, "/timetable/api/stationboard.json", { + stop: options.stationId, + limit: options.limit, + show_delays: 1, + show_tracks: 1, + date, + time, + }), + ); + if (!response.ok) throw new ResponseError(response); + + const data = await timetableSchema.parseAsync(await response.json()); + return { + stationId: options.stationId, + timestamp: now, + entries: data.connections.map((connection) => { + const color = connection.color.split("~")[0]; + return { + timestamp: connection.time, + line: connection.line + ? { + name: connection.line, + color: color && color.length >= 1 ? `#${color}` : null, + } + : null, + location: connection.terminal.name, + delay: connection.dep_delay, + platform: connection.track + ? { + name: connection.track.replace("!", ""), + hasChanged: connection.track.includes("!"), + } + : null, + }; + }), + }; +}; + +const timetableSchema = z.object({ + connections: z.array( + z.object({ + time: z.string().transform((str) => new Date(str)), + line: z.string().optional(), + color: z.string(), + terminal: z.object({ name: z.string() }), + track: z.string().optional(), + dep_delay: z + .string() + .optional() + .transform((str) => (str ? parseInt(str) : 0)), + }), + ), +}); + +const searchSchema = z.array( + z.object({ + id: z.string().optional(), + label: z.string(), + iconclass: z.string(), + }), +); diff --git a/packages/request-handler/src/tracearr.ts b/packages/request-handler/src/tracearr.ts index 8f593ac03..641bf2922 100644 --- a/packages/request-handler/src/tracearr.ts +++ b/packages/request-handler/src/tracearr.ts @@ -1,11 +1,9 @@ -import dayjs from "dayjs"; - import { createIntegrationAsync } from "@homarr/integrations"; import type { TracearrDashboardData } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const tracearrRequestHandler = createCachedIntegrationRequestHandler< +export const tracearrRequestHandler = createIntegrationRequestHandler< TracearrDashboardData, "tracearr", Record @@ -14,6 +12,4 @@ export const tracearrRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getDashboardDataAsync(); }, - cacheDuration: dayjs.duration(5, "seconds"), - queryKey: "tracearrDashboard", }); diff --git a/packages/request-handler/src/traefik.ts b/packages/request-handler/src/traefik.ts new file mode 100644 index 000000000..1b87e2bb1 --- /dev/null +++ b/packages/request-handler/src/traefik.ts @@ -0,0 +1,15 @@ +import { createIntegrationAsync } from "@homarr/integrations"; +import type { TraefikDashboardData } from "@homarr/integrations/types"; + +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; + +export const traefikRequestHandler = createIntegrationRequestHandler< + TraefikDashboardData, + "traefik", + Record +>({ + async requestAsync(integration, _input) { + const integrationInstance = await createIntegrationAsync(integration); + return await integrationInstance.getDashboardDataAsync(); + }, +}); diff --git a/packages/request-handler/src/umami.ts b/packages/request-handler/src/umami.ts index e943c568a..02ef71140 100644 --- a/packages/request-handler/src/umami.ts +++ b/packages/request-handler/src/umami.ts @@ -1,11 +1,9 @@ -import dayjs from "dayjs"; - import { createIntegrationAsync } from "@homarr/integrations"; import type { UmamiEventSeries, UmamiMetricItem, UmamiVisitorStats } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const umamiRequestHandler = createCachedIntegrationRequestHandler< +export const umamiRequestHandler = createIntegrationRequestHandler< UmamiVisitorStats, "umami", { websiteId: string; timeFrame: string; eventName?: string } @@ -14,24 +12,16 @@ export const umamiRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getVisitorStatsAsync(input.websiteId, input.timeFrame, input.eventName); }, - cacheDuration: dayjs.duration(5, "minutes"), - queryKey: "umamiVisitorStats", }); -export const umamiEventNamesRequestHandler = createCachedIntegrationRequestHandler< - string[], - "umami", - { websiteId: string } ->({ +export const umamiEventNamesRequestHandler = createIntegrationRequestHandler({ async requestAsync(integration, input) { const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getEventNamesAsync(input.websiteId); }, - cacheDuration: dayjs.duration(5, "minutes"), - queryKey: "umamiEventNames", }); -export const umamiTopPagesRequestHandler = createCachedIntegrationRequestHandler< +export const umamiTopPagesRequestHandler = createIntegrationRequestHandler< UmamiMetricItem[], "umami", { websiteId: string; timeFrame: string; limit: number } @@ -40,11 +30,9 @@ export const umamiTopPagesRequestHandler = createCachedIntegrationRequestHandler const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getTopPagesAsync(input.websiteId, input.timeFrame, input.limit); }, - cacheDuration: dayjs.duration(5, "minutes"), - queryKey: "umamiTopPages", }); -export const umamiTopReferrersRequestHandler = createCachedIntegrationRequestHandler< +export const umamiTopReferrersRequestHandler = createIntegrationRequestHandler< UmamiMetricItem[], "umami", { websiteId: string; timeFrame: string; limit: number } @@ -53,11 +41,9 @@ export const umamiTopReferrersRequestHandler = createCachedIntegrationRequestHan const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getTopReferrersAsync(input.websiteId, input.timeFrame, input.limit); }, - cacheDuration: dayjs.duration(5, "minutes"), - queryKey: "umamiTopReferrers", }); -export const umamiMultiEventRequestHandler = createCachedIntegrationRequestHandler< +export const umamiMultiEventRequestHandler = createIntegrationRequestHandler< UmamiEventSeries[], "umami", { websiteId: string; timeFrame: string; eventNames: string[] } @@ -66,11 +52,9 @@ export const umamiMultiEventRequestHandler = createCachedIntegrationRequestHandl const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getMultiEventTimeSeriesAsync(input.websiteId, input.timeFrame, input.eventNames); }, - cacheDuration: dayjs.duration(5, "minutes"), - queryKey: "umamiMultiEvent", }); -export const umamiActiveVisitorsRequestHandler = createCachedIntegrationRequestHandler< +export const umamiActiveVisitorsRequestHandler = createIntegrationRequestHandler< number, "umami", { websiteId: string } @@ -79,6 +63,4 @@ export const umamiActiveVisitorsRequestHandler = createCachedIntegrationRequestH const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getActiveVisitorsAsync(input.websiteId); }, - cacheDuration: dayjs.duration(30, "seconds"), - queryKey: "umamiActiveVisitors", }); diff --git a/packages/request-handler/src/update-checker.ts b/packages/request-handler/src/update-checker.ts index 8709f20c5..1cf4dbd6f 100644 --- a/packages/request-handler/src/update-checker.ts +++ b/packages/request-handler/src/update-checker.ts @@ -1,4 +1,3 @@ -import dayjs from "dayjs"; import { Octokit } from "octokit"; import { compareSemVer, isValidSemVer, parseSemVer } from "semver-parser"; @@ -6,27 +5,18 @@ import { env } from "@homarr/common/env"; import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { createLogger } from "@homarr/core/infrastructure/logs"; import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; -import { createChannelWithLatestAndEvents } from "@homarr/redis"; -import { createCachedRequestHandler } from "@homarr/request-handler/lib/cached-request-handler"; +import { createRequestHandler } from "./lib/request-handler"; import packageJson from "../../../package.json"; const logger = createLogger({ module: "updateCheckerRequestHandler" }); -export const updateCheckerRequestHandler = createCachedRequestHandler({ - queryKey: "homarr-update-checker", - cacheDuration: dayjs.duration(1, "day"), - fallbackToStaleOnError: true, +export const updateCheckerRequestHandler = createRequestHandler({ async requestAsync(_) { - return { - availableUpdates: await getAvailableUpdatesAsync(packageJson.version), - }; - }, - createRedisChannel() { - return createChannelWithLatestAndEvents<{ - availableUpdates: Update[]; - }>("homarr:update"); + const availableUpdates = await getAvailableUpdatesAsync(packageJson.version); + return { availableUpdates }; }, + cacheTtlMs: 30 * 60 * 1000, }); interface Update { diff --git a/packages/request-handler/src/ups.ts b/packages/request-handler/src/ups.ts index acc97eb6d..4ca17b5ee 100644 --- a/packages/request-handler/src/ups.ts +++ b/packages/request-handler/src/ups.ts @@ -1,12 +1,10 @@ -import dayjs from "dayjs"; - import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { UpsSummary } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const upsSummariesRequestHandler = createCachedIntegrationRequestHandler< +export const upsSummariesRequestHandler = createIntegrationRequestHandler< UpsSummary[], IntegrationKindByCategory<"ups">, Record @@ -15,6 +13,4 @@ export const upsSummariesRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getUpsSummariesAsync(); }, - cacheDuration: dayjs.duration(1, "minute"), - queryKey: "upsSummaries", }); diff --git a/packages/request-handler/src/uptime-kuma.ts b/packages/request-handler/src/uptime-kuma.ts index 39149c137..b9453eb76 100644 --- a/packages/request-handler/src/uptime-kuma.ts +++ b/packages/request-handler/src/uptime-kuma.ts @@ -1,11 +1,9 @@ -import dayjs from "dayjs"; - import { createIntegrationAsync } from "@homarr/integrations"; import type { UptimeKumaDashboardData } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const uptimeKumaRequestHandler = createCachedIntegrationRequestHandler< +export const uptimeKumaRequestHandler = createIntegrationRequestHandler< UptimeKumaDashboardData, "uptimeKuma", Record @@ -14,6 +12,4 @@ export const uptimeKumaRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getDashboardDataAsync(); }, - cacheDuration: dayjs.duration(1, "minute"), - queryKey: "uptimeKumaDashboard", }); diff --git a/packages/request-handler/src/vpn.ts b/packages/request-handler/src/vpn.ts index de068938c..ebca0fbeb 100644 --- a/packages/request-handler/src/vpn.ts +++ b/packages/request-handler/src/vpn.ts @@ -1,15 +1,13 @@ -import dayjs from "dayjs"; - import { createLogger } from "@homarr/core/infrastructure/logs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { VpnSummary } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; const logger = createLogger({ module: "vpnRequestHandler" }); -export const vpnSummaryHandler = createCachedIntegrationRequestHandler< +export const vpnSummaryHandler = createIntegrationRequestHandler< VpnSummary | null, IntegrationKindByCategory<"vpn">, Record @@ -19,9 +17,6 @@ export const vpnSummaryHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getSummaryAsync(); } catch (error) { - // The VPN control server is unreachable (e.g. the container is down). - // Return null so the widget can render an "unavailable" status instead of - // surfacing a hard error, and so it recovers automatically once it is back up. logger.warn("Failed to fetch VPN summary, marking as unavailable", { integrationId: integration.id, integrationKind: integration.kind, @@ -30,6 +25,4 @@ export const vpnSummaryHandler = createCachedIntegrationRequestHandler< return null; } }, - cacheDuration: dayjs.duration(5, "minutes"), - queryKey: "vpnSummary", }); diff --git a/packages/request-handler/src/weather.ts b/packages/request-handler/src/weather.ts index eb4828048..5f5a097a4 100644 --- a/packages/request-handler/src/weather.ts +++ b/packages/request-handler/src/weather.ts @@ -1,14 +1,11 @@ -import dayjs from "dayjs"; import { z } from "zod"; import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; -import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; +import { createWidgetRequestHandler } from "./lib/widget-request-handler"; -export const weatherRequestHandler = createCachedWidgetRequestHandler({ - queryKey: "weatherAtLocation", - widgetKind: "weather", +export const weatherRequestHandler = createWidgetRequestHandler({ async requestAsync(input: { latitude: number; longitude: number }) { const res = await withTimeoutAsync(async (signal) => { return await fetchWithTrustedCertificatesAsync( @@ -34,7 +31,6 @@ export const weatherRequestHandler = createCachedWidgetRequestHandler({ }), } satisfies Weather; }, - cacheDuration: dayjs.duration(1, "minute"), }); const atLocationOutput = z.object({ diff --git a/packages/translation/src/lang/ca.json b/packages/translation/src/lang/ca.json index 18d07473d..e227775fb 100644 --- a/packages/translation/src/lang/ca.json +++ b/packages/translation/src/lang/ca.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "", "speedtest": "", "analytics": "", + "vpn": "", "mediaLibrary": "", "uptimeMonitoring": "" }, @@ -1412,7 +1413,10 @@ "here": "", "iconPicker": { "label": "", - "header": "" + "header": "", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "", "switchToLightMode": "", "management": "", + "docker": "", "preferences": "", "logout": "", "login": "", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "" + "settings": "", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "", "noStation": "", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -2352,6 +2368,9 @@ "name": "", "description": "", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "" }, @@ -2405,6 +2424,12 @@ "title": "" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "", "description": "" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "", @@ -3133,6 +3162,9 @@ "provider": { "label": "" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "", "placeholder": "" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "", "tooltip": "" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "", "noMatchingVersion": "", "noReleasesFound": "", - "noProviderSeleceted": "", - "noProviderResponse": "" + "noProviderSelected": "", + "noProviderResponse": "", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "", "text": "" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "", @@ -4499,86 +4555,14 @@ "disabled": "" }, "job": { - "minecraftServerStatus": { - "label": "" - }, - "timetable": { - "label": "" - }, "iconsUpdater": { "label": "" }, "analytics": { "label": "" }, - "smartHomeEntityState": { - "label": "" - }, "ping": { "label": "" - }, - "mediaServer": { - "label": "" - }, - "mediaOrganizer": { - "label": "" - }, - "downloads": { - "label": "" - }, - "mediaRequestStats": { - "label": "" - }, - "mediaRequestList": { - "label": "" - }, - "rssFeeds": { - "label": "" - }, - "indexerManager": { - "label": "" - }, - "healthMonitoring": { - "label": "" - }, - "dnsHole": { - "label": "" - }, - "updateChecker": { - "label": "" - }, - "mediaTranscoding": { - "label": "" - }, - "networkController": { - "label": "" - }, - "refreshNotifications": { - "label": "" - }, - "dockerContainers": { - "label": "" - }, - "firewallCpu": { - "label": "" - }, - "firewallMemory": { - "label": "" - }, - "firewallVersion": { - "label": "" - }, - "firewallInterfaces": { - "label": "" - }, - "weather": { - "label": "" - }, - "tracearr": { - "label": "" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "" + "internalServerError": "", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/cn.json b/packages/translation/src/lang/cn.json index 04f93c6b9..aa0baf940 100644 --- a/packages/translation/src/lang/cn.json +++ b/packages/translation/src/lang/cn.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "", "speedtest": "", "analytics": "", + "vpn": "", "mediaLibrary": "", "uptimeMonitoring": "" }, @@ -1412,7 +1413,10 @@ "here": "这里", "iconPicker": { "label": "图标 URL", - "header": "输入名称或对象来筛选图标… Homarr 将为您搜索 {countIcons} 图标。" + "header": "输入名称或对象来筛选图标… Homarr 将为您搜索 {countIcons} 图标。", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "切换到暗黑模式", "switchToLightMode": "切换到日间模式", "management": "管理中心", + "docker": "", "preferences": "偏好设置", "logout": "退出登录", "login": "登录", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "设置" + "settings": "设置", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "出发时间 {station}", "noStation": "未选择车站", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "车站" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "显示文件系统信息" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "默认标签" }, @@ -2352,6 +2368,9 @@ "name": "Docker 状态", "description": "您的容器状态 (这个小部件只能用管理员权限添加)", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "启用项目排序" }, @@ -2405,6 +2424,12 @@ "title": "实例" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "显示图片信息", "description": "在幻灯片底部显示图片日期和当前索引" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "Immich 相册图片幻灯片", @@ -3133,6 +3162,9 @@ "provider": { "label": "提供者" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "标识符", "placeholder": "姓名或所有者名称" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "没有提供者", "tooltip": "无法解析该提供者,请在导入镜像后手动设置" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "无效标识符", "noMatchingVersion": "没有找到匹配的版本", "noReleasesFound": "没有发现发布信息", - "noProviderSeleceted": "没有选择提供者", - "noProviderResponse": "提供者没有响应" + "noProviderSelected": "", + "noProviderResponse": "提供者没有响应", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "显示背景使用栏" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "未找到", "text": "找不到请求的资源" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "首页", @@ -4499,86 +4555,14 @@ "disabled": "禁用" }, "job": { - "minecraftServerStatus": { - "label": "Minecraft 服务状态" - }, - "timetable": { - "label": "时刻表" - }, "iconsUpdater": { "label": "图标更新" }, "analytics": { "label": "分析" }, - "smartHomeEntityState": { - "label": "智能家居实体状态" - }, "ping": { "label": "Pings" - }, - "mediaServer": { - "label": "媒体服务" - }, - "mediaOrganizer": { - "label": "媒体组织者" - }, - "downloads": { - "label": "正在下载" - }, - "mediaRequestStats": { - "label": "媒体请求统计" - }, - "mediaRequestList": { - "label": "媒体请求列表" - }, - "rssFeeds": { - "label": "RSS订阅" - }, - "indexerManager": { - "label": "索引器管理" - }, - "healthMonitoring": { - "label": "健康监测" - }, - "dnsHole": { - "label": "DNS Hole 数据" - }, - "updateChecker": { - "label": "更新检查" - }, - "mediaTranscoding": { - "label": "媒体转码" - }, - "networkController": { - "label": "网络控制器" - }, - "refreshNotifications": { - "label": "更新通知" - }, - "dockerContainers": { - "label": "Docker 容器" - }, - "firewallCpu": { - "label": "防火墙 CPU" - }, - "firewallMemory": { - "label": "防火墙内存" - }, - "firewallVersion": { - "label": "防火墙版本" - }, - "firewallInterfaces": { - "label": "防火墙接口" - }, - "weather": { - "label": "天气" - }, - "tracearr": { - "label": "Tracearr" - }, - "umamiActiveVisitors": { - "label": "Umami 活跃访客" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "获取 Docker 容器失败" + "internalServerError": "获取 Docker 容器失败", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/cr.json b/packages/translation/src/lang/cr.json index 9341366dc..3a4ae4319 100644 --- a/packages/translation/src/lang/cr.json +++ b/packages/translation/src/lang/cr.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "crwdns4473:0crwdne4473:0", "speedtest": "crwdns4475:0crwdne4475:0", "analytics": "crwdns4477:0crwdne4477:0", + "vpn": "crwdns5687:0crwdne5687:0", "mediaLibrary": "crwdns4979:0crwdne4979:0", "uptimeMonitoring": "crwdns4981:0crwdne4981:0" }, @@ -1412,7 +1413,10 @@ "here": "crwdns552:0crwdne552:0", "iconPicker": { "label": "crwdns554:0crwdne554:0", - "header": "crwdns556:0{countIcons}crwdne556:0" + "header": "crwdns556:0{countIcons}crwdne556:0", + "headerLoading": "crwdns5681:0crwdne5681:0", + "noResults": "crwdns5683:0{search}crwdne5683:0", + "error": "crwdns5685:0crwdne5685:0" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "crwdns594:0crwdne594:0", "switchToLightMode": "crwdns596:0crwdne596:0", "management": "crwdns598:0crwdne598:0", + "docker": "crwdns5703:0crwdne5703:0", "preferences": "crwdns600:0crwdne600:0", "logout": "crwdns602:0crwdne602:0", "login": "crwdns604:0crwdne604:0", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "crwdns702:0crwdne702:0" + "settings": "crwdns702:0crwdne702:0", + "refresh": "crwdns5739:0crwdne5739:0", + "options": "crwdns5709:0crwdne5709:0", + "actions": "crwdns5711:0crwdne5711:0" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "crwdns4004:0{station}crwdne4004:0", "noStation": "crwdns4006:0crwdne4006:0", "option": { + "baseUrl": { + "label": "crwdns5713:0crwdne5713:0" + }, "station": { "label": "crwdns4008:0crwdne4008:0" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "crwdns1082:0crwdne1082:0" }, + "visibleStorageVolumes": { + "label": "crwdns5741:0crwdne5741:0", + "description": "crwdns5743:0crwdne5743:0", + "placeholder": "crwdns5745:0crwdne5745:0" + }, "defaultTab": { "label": "crwdns2636:0crwdne2636:0" }, @@ -2352,6 +2368,9 @@ "name": "crwdns3424:0crwdne3424:0", "description": "crwdns3426:0crwdne3426:0", "option": { + "columns": { + "label": "crwdns5705:0crwdne5705:0" + }, "enableRowSorting": { "label": "crwdns3808:0crwdne3808:0" }, @@ -2405,6 +2424,12 @@ "title": "crwdns3876:0crwdne3876:0" } }, + "vpn": { + "name": "crwdns5689:0crwdne5689:0", + "description": "crwdns5691:0crwdne5691:0", + "serviceUnavailable": "crwdns5693:0crwdne5693:0", + "option": {} + }, "ups": { "name": "crwdns4481:0crwdne4481:0", "description": "crwdns4483:0crwdne4483:0", @@ -2759,8 +2784,8 @@ "name": "crwdns3908:0crwdne3908:0", "option": { "albumId": { - "label": "crwdns3910:0crwdne3910:0", - "description": "crwdns3912:0crwdne3912:0" + "label": "crwdns5715:0crwdne5715:0", + "description": "crwdns5717:0crwdne5717:0" }, "rotationIntervalSeconds": { "label": "crwdns3914:0crwdne3914:0", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "crwdns3918:0crwdne3918:0", "description": "crwdns3920:0crwdne3920:0" + }, + "randomizePhotos": { + "label": "crwdns5719:0crwdne5719:0", + "description": "crwdns5721:0crwdne5721:0" } }, "description": "crwdns3922:0crwdne3922:0", @@ -3133,6 +3162,9 @@ "provider": { "label": "crwdns3204:0crwdne3204:0" }, + "providerUrl": { + "label": "crwdns5723:0crwdne5723:0" + }, "identifier": { "label": "crwdns3206:0crwdne3206:0", "placeholder": "crwdns3208:0crwdne3208:0" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "crwdns3508:0crwdne3508:0", "tooltip": "crwdns3510:0crwdne3510:0" + }, + "tokens": { + "label": "crwdns5725:0crwdne5725:0", + "token": "crwdns5727:0crwdne5727:0", + "configured": "crwdns5729:0crwdne5729:0", + "notConfigured": "crwdns5731:0crwdne5731:0", + "save": "crwdns5733:0crwdne5733:0" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "crwdns3512:0crwdne3512:0", "noMatchingVersion": "crwdns3514:0crwdne3514:0", "noReleasesFound": "crwdns3516:0crwdne3516:0", - "noProviderSeleceted": "crwdns3518:0crwdne3518:0", - "noProviderResponse": "crwdns3520:0crwdne3520:0" + "noProviderSelected": "crwdns5735:0crwdne5735:0", + "noProviderResponse": "crwdns3520:0crwdne3520:0", + "unexpected": "crwdns5737:0crwdne5737:0" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "crwdns3842:0crwdne3842:0" + }, + "visibleStorageVolumes": { + "label": "crwdns5747:0crwdne5747:0", + "description": "crwdns5749:0crwdne5749:0", + "placeholder": "crwdns5751:0crwdne5751:0" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "crwdns5653:0crwdne5653:0" }, + "empty": { + "noSystems": "crwdns5677:0crwdne5677:0" + }, "metric": { "cpu": "crwdns4865:0crwdne4865:0", "memory": "crwdns4867:0crwdne4867:0", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "crwdns4927:0crwdne4927:0", + "empty": { + "noSystems": "crwdns5679:0crwdne5679:0" + }, "error": { "internalServerError": "crwdns5661:0crwdne5661:0", "liveConnectionFailed": "crwdns5663:0crwdne5663:0", @@ -4133,6 +4184,11 @@ "title": "crwdns1568:0crwdne1568:0", "text": "crwdns1570:0crwdne1570:0" }, + "error": { + "title": "crwdns5697:0crwdne5697:0", + "text": "crwdns5699:0crwdne5699:0", + "digest": "crwdns5701:0{digest}crwdne5701:0" + }, "navbar": { "items": { "home": "crwdns1572:0crwdne1572:0", @@ -4499,86 +4555,14 @@ "disabled": "crwdns3484:0crwdne3484:0" }, "job": { - "minecraftServerStatus": { - "label": "crwdns2496:0crwdne2496:0" - }, - "timetable": { - "label": "crwdns4010:0crwdne4010:0" - }, "iconsUpdater": { "label": "crwdns1828:0crwdne1828:0" }, "analytics": { "label": "crwdns1830:0crwdne1830:0" }, - "smartHomeEntityState": { - "label": "crwdns1832:0crwdne1832:0" - }, "ping": { "label": "crwdns1834:0crwdne1834:0" - }, - "mediaServer": { - "label": "crwdns1836:0crwdne1836:0" - }, - "mediaOrganizer": { - "label": "crwdns1838:0crwdne1838:0" - }, - "downloads": { - "label": "crwdns1840:0crwdne1840:0" - }, - "mediaRequestStats": { - "label": "crwdns1842:0crwdne1842:0" - }, - "mediaRequestList": { - "label": "crwdns1844:0crwdne1844:0" - }, - "rssFeeds": { - "label": "crwdns1846:0crwdne1846:0" - }, - "indexerManager": { - "label": "crwdns1848:0crwdne1848:0" - }, - "healthMonitoring": { - "label": "crwdns1850:0crwdne1850:0" - }, - "dnsHole": { - "label": "crwdns1852:0crwdne1852:0" - }, - "updateChecker": { - "label": "crwdns2258:0crwdne2258:0" - }, - "mediaTranscoding": { - "label": "crwdns2436:0crwdne2436:0" - }, - "networkController": { - "label": "crwdns3153:0crwdne3153:0" - }, - "refreshNotifications": { - "label": "crwdns3482:0crwdne3482:0" - }, - "dockerContainers": { - "label": "crwdns3430:0crwdne3430:0" - }, - "firewallCpu": { - "label": "crwdns3592:0crwdne3592:0" - }, - "firewallMemory": { - "label": "crwdns3594:0crwdne3594:0" - }, - "firewallVersion": { - "label": "crwdns3596:0crwdne3596:0" - }, - "firewallInterfaces": { - "label": "crwdns3598:0crwdne3598:0" - }, - "weather": { - "label": "crwdns3724:0crwdne3724:0" - }, - "tracearr": { - "label": "crwdns3990:0crwdne3990:0" - }, - "umamiActiveVisitors": { - "label": "crwdns4223:0crwdne4223:0" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "crwdns2262:0crwdne2262:0" + "internalServerError": "crwdns2262:0crwdne2262:0", + "noColumns": "crwdns5707:0crwdne5707:0" } }, "kubernetes": { diff --git a/packages/translation/src/lang/cs.json b/packages/translation/src/lang/cs.json index 89b8949bb..016c40061 100644 --- a/packages/translation/src/lang/cs.json +++ b/packages/translation/src/lang/cs.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "", "speedtest": "", "analytics": "", + "vpn": "", "mediaLibrary": "", "uptimeMonitoring": "" }, @@ -1412,7 +1413,10 @@ "here": "zde", "iconPicker": { "label": "URL ikony", - "header": "" + "header": "", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "Přepnout do tmavého režimu", "switchToLightMode": "Přepnout do světlého režimu", "management": "Správa", + "docker": "", "preferences": "Vaše předvolby", "logout": "Odhlásit se", "login": "Přihlásit se", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "Nastavení" + "settings": "Nastavení", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "", "noStation": "", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "Zobrazit informace o souborovém systému" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -2352,6 +2368,9 @@ "name": "", "description": "", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "" }, @@ -2405,6 +2424,12 @@ "title": "" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "", "description": "" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "", @@ -3133,6 +3162,9 @@ "provider": { "label": "" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "", "placeholder": "" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "", "tooltip": "" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "", "noMatchingVersion": "", "noReleasesFound": "", - "noProviderSeleceted": "", - "noProviderResponse": "" + "noProviderSelected": "", + "noProviderResponse": "", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "", "text": "Nepodařilo se nalézt požadovaný zdroj" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "Domovská stránka", @@ -4499,86 +4555,14 @@ "disabled": "" }, "job": { - "minecraftServerStatus": { - "label": "" - }, - "timetable": { - "label": "" - }, "iconsUpdater": { "label": "" }, "analytics": { "label": "" }, - "smartHomeEntityState": { - "label": "" - }, "ping": { "label": "" - }, - "mediaServer": { - "label": "Mediální server" - }, - "mediaOrganizer": { - "label": "" - }, - "downloads": { - "label": "" - }, - "mediaRequestStats": { - "label": "" - }, - "mediaRequestList": { - "label": "" - }, - "rssFeeds": { - "label": "" - }, - "indexerManager": { - "label": "Správce indexeru" - }, - "healthMonitoring": { - "label": "" - }, - "dnsHole": { - "label": "" - }, - "updateChecker": { - "label": "" - }, - "mediaTranscoding": { - "label": "" - }, - "networkController": { - "label": "" - }, - "refreshNotifications": { - "label": "" - }, - "dockerContainers": { - "label": "" - }, - "firewallCpu": { - "label": "" - }, - "firewallMemory": { - "label": "" - }, - "firewallVersion": { - "label": "" - }, - "firewallInterfaces": { - "label": "" - }, - "weather": { - "label": "" - }, - "tracearr": { - "label": "" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "" + "internalServerError": "", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/da.json b/packages/translation/src/lang/da.json index d22ef9c61..bbb6da4c6 100644 --- a/packages/translation/src/lang/da.json +++ b/packages/translation/src/lang/da.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "", "speedtest": "", "analytics": "", + "vpn": "", "mediaLibrary": "", "uptimeMonitoring": "" }, @@ -1412,7 +1413,10 @@ "here": "her", "iconPicker": { "label": "Ikon-URL", - "header": "Indtast navn eller objekter for at filtrere for ikoner... Homarr vil søge gennem {countIcons} ikoner for dig." + "header": "Indtast navn eller objekter for at filtrere for ikoner... Homarr vil søge gennem {countIcons} ikoner for dig.", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "Skift til mørk tilstand", "switchToLightMode": "Skift til lys tilstand", "management": "Håndtering", + "docker": "", "preferences": "Dine indstillinger", "logout": "Log ud", "login": "Log ind", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "Indstillinger" + "settings": "Indstillinger", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "Afgange {station}", "noStation": "Ingen station valgt", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "Station" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "Vis information om filsystemet" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Standard fane" }, @@ -2352,6 +2368,9 @@ "name": "Docker statistik", "description": "Statistik for dine containere (Denne widget kan kun tilføjes med administratorrettigheder)", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "Aktiver sortering" }, @@ -2405,6 +2424,12 @@ "title": "Instanser" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "Vis billedinfo", "description": "Vis dato for billedet og det aktuelle indeks i bunden af diasshowet" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "Diasshow af billeder i dit Immich album", @@ -3133,6 +3162,9 @@ "provider": { "label": "Udbyder" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "Identifikator", "placeholder": "Navn eller ejer/navn" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "Ingen Udbyder", "tooltip": "Udbyderen kunne ikke fortolkes, angiv venligst det manuelt efter import af billederne" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "Ugyldig identifikator", "noMatchingVersion": "Ingen matchende version fundet", "noReleasesFound": "Ingen udgivelser fundet", - "noProviderSeleceted": "Ingen udbyder valgt", - "noProviderResponse": "Intet svar fra udbyder" + "noProviderSelected": "", + "noProviderResponse": "Intet svar fra udbyder", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "Vis forbrugslinje i baggrunden" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "Ikke Fundet", "text": "Kunne ikke finde den anmodede ressource" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "Hjem", @@ -4499,86 +4555,14 @@ "disabled": "Deaktiveret" }, "job": { - "minecraftServerStatus": { - "label": "Status for Minecraft server" - }, - "timetable": { - "label": "Tidsskema" - }, "iconsUpdater": { "label": "Opdatering Af Ikoner" }, "analytics": { "label": "Analyser" }, - "smartHomeEntityState": { - "label": "Smarthjem ententitets tilstand" - }, "ping": { "label": "Pings" - }, - "mediaServer": { - "label": "Medieserver" - }, - "mediaOrganizer": { - "label": "Medieorganisatorer" - }, - "downloads": { - "label": "Downloads" - }, - "mediaRequestStats": { - "label": "Statistik For Medieforespørgsel" - }, - "mediaRequestList": { - "label": "Liste Over Medieforespørgsler" - }, - "rssFeeds": { - "label": "RSS feeds" - }, - "indexerManager": { - "label": "Indekser Manager" - }, - "healthMonitoring": { - "label": "Helbredsovervågning" - }, - "dnsHole": { - "label": "DNS Hole Data" - }, - "updateChecker": { - "label": "Opdaterings checker" - }, - "mediaTranscoding": { - "label": "Omkodning af medier" - }, - "networkController": { - "label": "Netværkskontroller" - }, - "refreshNotifications": { - "label": "Notifikations opdaterer" - }, - "dockerContainers": { - "label": "Docker containers" - }, - "firewallCpu": { - "label": "Firewall CPU" - }, - "firewallMemory": { - "label": "Firewall Hukommelse" - }, - "firewallVersion": { - "label": "Firewall Version" - }, - "firewallInterfaces": { - "label": "Firewall Grænseflader" - }, - "weather": { - "label": "Vejr" - }, - "tracearr": { - "label": "Tracearr" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "Kunne ikke hente Docker-containere" + "internalServerError": "Kunne ikke hente Docker-containere", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/de-CH.json b/packages/translation/src/lang/de-CH.json index bbe34e1c0..4c9c8f313 100644 --- a/packages/translation/src/lang/de-CH.json +++ b/packages/translation/src/lang/de-CH.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "", "speedtest": "", "analytics": "", + "vpn": "", "mediaLibrary": "", "uptimeMonitoring": "" }, @@ -1412,7 +1413,10 @@ "here": "Hier", "iconPicker": { "label": "", - "header": "Geben Sie Namen oder Objekte ein, um nach Symbolen zu filtern... Homarr sucht nach {countIcons} Icons für Sie." + "header": "Geben Sie Namen oder Objekte ein, um nach Symbolen zu filtern... Homarr sucht nach {countIcons} Icons für Sie.", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "Zum dunklen Design wechseln", "switchToLightMode": "Zum hellen Design wechseln", "management": "Verwaltung", + "docker": "", "preferences": "Ihre Einstellungen", "logout": "Abmelden", "login": "Anmelden", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "Einstellungen" + "settings": "Einstellungen", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "", "noStation": "", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "Dateisystem Info anzeigen" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Standard Tab" }, @@ -2352,6 +2368,9 @@ "name": "", "description": "", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "" }, @@ -2405,6 +2424,12 @@ "title": "" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "", "description": "" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "", @@ -3133,6 +3162,9 @@ "provider": { "label": "" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "", "placeholder": "" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "", "tooltip": "" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "", "noMatchingVersion": "", "noReleasesFound": "", - "noProviderSeleceted": "", - "noProviderResponse": "" + "noProviderSelected": "", + "noProviderResponse": "", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "Nicht gefunden", "text": "Konnte die angeforderte Ressource nicht finden" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "Startseite", @@ -4499,86 +4555,14 @@ "disabled": "" }, "job": { - "minecraftServerStatus": { - "label": "Status des Minecraft Servers" - }, - "timetable": { - "label": "" - }, "iconsUpdater": { "label": "Symbol Updater" }, "analytics": { "label": "Nutzungsdaten" }, - "smartHomeEntityState": { - "label": "Status der Smart Home Entitäten" - }, "ping": { "label": "" - }, - "mediaServer": { - "label": "Medien Server" - }, - "mediaOrganizer": { - "label": "Medienorganisatoren" - }, - "downloads": { - "label": "" - }, - "mediaRequestStats": { - "label": "Statistik der Medienanfragen" - }, - "mediaRequestList": { - "label": "Liste der Medienanfragen" - }, - "rssFeeds": { - "label": "RSS Feeds" - }, - "indexerManager": { - "label": "Index Manager" - }, - "healthMonitoring": { - "label": "Überwachung des Gesundheit" - }, - "dnsHole": { - "label": "DNS Hole Daten" - }, - "updateChecker": { - "label": "Updateprüfer" - }, - "mediaTranscoding": { - "label": "Medien Transkodierung" - }, - "networkController": { - "label": "" - }, - "refreshNotifications": { - "label": "" - }, - "dockerContainers": { - "label": "" - }, - "firewallCpu": { - "label": "" - }, - "firewallMemory": { - "label": "" - }, - "firewallVersion": { - "label": "" - }, - "firewallInterfaces": { - "label": "" - }, - "weather": { - "label": "" - }, - "tracearr": { - "label": "" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "Fehler beim Abrufen der Docker Container" + "internalServerError": "Fehler beim Abrufen der Docker Container", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/de.json b/packages/translation/src/lang/de.json index 09d4a4ce2..086b3ea36 100644 --- a/packages/translation/src/lang/de.json +++ b/packages/translation/src/lang/de.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "Überwachung", "speedtest": "Geschwindigkeitstest", "analytics": "Nutzungsdaten", + "vpn": "", "mediaLibrary": "Medienbibliothek", "uptimeMonitoring": "Überwachung der Betriebszeit" }, @@ -1412,7 +1413,10 @@ "here": "Hier", "iconPicker": { "label": "Icon URL", - "header": "Geben Sie Namen oder Objekte ein, um nach Symbolen zu filtern... Homarr sucht nach {countIcons} Icons für Sie." + "header": "Geben Sie Namen oder Objekte ein, um nach Symbolen zu filtern... Homarr sucht nach {countIcons} Icons für Sie.", + "headerLoading": "Tippe um die Symbole zu durchsuchen…", + "noResults": "Keine Symbole stimmen mit \"{search}\" überein.", + "error": "Konnte Symbole nicht laden." }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "Zum dunklen Design wechseln", "switchToLightMode": "Zum hellen Design wechseln", "management": "Verwaltung", + "docker": "", "preferences": "Ihre Einstellungen", "logout": "Abmelden", "login": "Anmelden", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "Einstellungen" + "settings": "Einstellungen", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "{station} Abfahrt", "noStation": "Keine Station ausgewählt", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "Station" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "Dateisystem Info anzeigen" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Standard Tab" }, @@ -2352,6 +2368,9 @@ "name": "Docker Statistiken", "description": "Statistiken Ihrer Container (Dieses Widget kann nur mit Administratorrechten hinzugefügt werden)", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "Sortierung von Elementen aktivieren" }, @@ -2405,6 +2424,12 @@ "title": "Instanzen" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "UPS", "description": "Überwachen Sie den Status Ihrer UPS Geräte über einen NUT Server", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "Foto Infos anzeigen", "description": "Datum des Bildes und des aktuellen Indexes am unteren Rand der Diashow anzeigen" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "Diashow der Bilder in Ihrem Immich Album", @@ -3133,6 +3162,9 @@ "provider": { "label": "Anbieter" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "Identifier", "placeholder": "Name oder Eigentümer/Name" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "Keine Anbieter", "tooltip": "Der Anbieter konnte nicht festgestellt werden, bitte manuell nach dem Import der Bilder festlegen" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "Ungültige Kennung", "noMatchingVersion": "Keine passende Version gefunden", "noReleasesFound": "Keine Veröffentlichungen gefunden", - "noProviderSeleceted": "Kein Anbieter ausgewählt", - "noProviderResponse": "Keine Antwort vom Anbieter" + "noProviderSelected": "", + "noProviderResponse": "Keine Antwort vom Anbieter", + "unexpected": "" } } }, @@ -3395,53 +3435,53 @@ }, "showNowPlaying": { "label": "\"Sie hören\" anzeigen", - "description": "" + "description": "Zeige aktuell gespielte Titel an (Navidrome)" }, "showNowPlayingList": { - "label": "", - "description": "" + "label": "\"Sie hören jetzt\" Wiedergabeliste anzeigen", + "description": "Einzelne Tracks anstelle der aktiven Streamanzahl anzeigen (Navidrome)" }, "maxNowPlayingItems": { - "label": "", - "description": "" + "label": "Maximale jetzt abgespielte Elemente", + "description": "Maximale Anzahl an Titeln, die in der \"Sie hören jetzt\" Wiedergabeliste angezeigt werden (Navidrome)" }, "showLibraryCount": { - "label": "", - "description": "" + "label": "Anzahl der Bibliotheken anzeigen", + "description": "Zeige die Anzahl der Bibliotheken in deiner Instanz (Hörbücherregal)" }, "showAudiobooks": { - "label": "", - "description": "" + "label": "Hörbücher anzeigen", + "description": "Zeige die Gesamtzahl der Hörbücher in allen Bibliotheken (Hörbücherregal)" }, "showPodcasts": { - "label": "", - "description": "" + "label": "Podcasts anzeigen", + "description": "Zeige die Gesamtzahl der Podcasts in allen Bibliotheken (Hörbücherregal)" }, "showListeningTime": { - "label": "", + "label": "Hördauer anzeigen", "description": "Zeigt deine gesamte Hörzeit (Audiobookshelf)" }, "showActiveSessions": { "label": "Aktive Streams anzeigen", - "description": "" + "description": "Zeige die Anzahl der aktuell aktiven Wiedergabesitzungen (Hörbücherregal)" }, "compactMode": { - "label": "", - "description": "" + "label": "Kompakter Modus", + "description": "Verwenden Sie ein dichteres Layout mit kleineren Kacheln, ideal für kleine Widget-Größen" } }, - "artists": "", - "albums": "", - "songs": "", - "libraryCount": "", - "audiobooks": "", - "podcasts": "", - "listeningTime": "", - "activeSessions": "", - "nowPlaying": "", - "nowPlayingCount": "", + "artists": "Künstler", + "albums": "Alben", + "songs": "Musik", + "libraryCount": "Bibliotheken", + "audiobooks": "Hörbücher", + "podcasts": "Podcasts", + "listeningTime": "Hördauer", + "activeSessions": "Aktive Sitzungen", + "nowPlaying": "Aktuelle Wiedergabe", + "nowPlayingCount": "{count, plural, one {# stream} other {# streams}}", "error": { - "internalServerError": "" + "internalServerError": "Fehler beim Abrufen der Audio-Statistikdaten" } }, "systemResources": { @@ -3457,7 +3497,7 @@ "option": { "cpu": "CPU", "memory": "Speicher", - "gpu": "", + "gpu": "GPU", "network": "Netzwerk" } }, @@ -3474,7 +3514,7 @@ "card": { "cpu": "CPU", "memory": "Arbeitsspeicher", - "gpu": "", + "gpu": "GPU", "network": "Netzwerk", "up": "OBEN", "down": "UNTEN" @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "Zeige Hintergrundnutzungsleiste" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3510,93 +3555,93 @@ } }, "customApi": { - "name": "", - "description": "", + "name": "Benutzerdefinierte API", + "description": "Zeigt Daten von jeder externen API mit benutzerdefinierter Konfiguration an.", "option": { "definitionId": { - "label": "" + "label": "Definitions ID" }, "refreshInterval": { - "label": "", - "description": "" + "label": "Aktualisierungsintervall", + "description": "Wie oft Daten von der API neu geladen werden sollen (in Sekunden)" } }, - "noDefinition": "", - "definitionNotFound": "", - "noDisplayOutput": "", - "executeConfirm": "", - "executing": "", - "executeSuccess": "", - "executeFailed": "" + "noDefinition": "Keine Definition ausgewählt", + "definitionNotFound": "Widget-Definition nicht gefunden. Möglicherweise wurde sie gelöscht.", + "noDisplayOutput": "Keine Anzeige-Ausgabe", + "executeConfirm": "Bist du dir sicher, dass du diese Aktion ausführen möchtest?", + "executing": "Wird ausgeführt...", + "executeSuccess": "Aktion erfolgreich ausgeführt", + "executeFailed": "Aktion fehlgeschlagen. Überprüfen Sie die Widget-Konfiguration." }, "beszelSystemTable": { - "name": "", - "description": "", + "name": "Beszel Systeme (Tabelle)", + "description": "Tabellenansicht aller Beszel-überwachten Systeme mit sortierbaren Spalten und Statusindikatoren", "option": { "statusFilter": { - "label": "" + "label": "Status Filter" }, "sortBy": { - "label": "" + "label": "Sortieren nach" }, "sortDirection": { - "label": "" + "label": "Sortierreihenfolge" }, "showCpu": { - "label": "" + "label": "CPU anzeigen" }, "showMemory": { - "label": "" + "label": "Arbeitsspeicher anzeigen" }, "showDisk": { - "label": "" + "label": "Datenträger anzeigen" }, "showGpu": { - "label": "" + "label": "GPU anzeigen" }, "showLoadAvg": { - "label": "" + "label": "Zeige Belastungsdurchschnitt" }, "showNet": { - "label": "" + "label": "Netzwerk anzeigen" }, "showTemp": { - "label": "" + "label": "Temperatur anzeigen" }, "showBattery": { - "label": "" + "label": "Akkustand anzeigen" }, "showServices": { - "label": "" + "label": "Dienste anzeigen" }, "showUptime": { - "label": "" + "label": "Laufzeit anzeigen" }, "showAgent": { - "label": "" + "label": "Agent-Version anzeigen" } }, - "noRecords": "", + "noRecords": "Keine Systeme gefunden", "error": { - "internalServerError": "" + "internalServerError": "Keine Verbindung zu Beszel. Überprüfen Sie Ihre Integrationseinstellungen." }, "column": { - "system": "", - "cpu": "", - "memory": "", - "disk": "", - "gpu": "", - "loadAvg": "", - "net": "", - "temp": "", - "battery": "", - "services": "", - "uptime": "", - "agent": "" + "system": "System", + "cpu": "CPU", + "memory": "Arbeitsspeicher", + "disk": "Datenträger", + "gpu": "GPU", + "loadAvg": "Systemlast", + "net": "Netz", + "temp": "Temperatur", + "battery": "Batterie", + "services": "Dienste", + "uptime": "Betriebszeit", + "agent": "Agent" } }, "beszelSystemGrid": { - "name": "", + "name": "Beszel Systems (Grid)", "description": "", "option": { "statusFilter": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "CPU", "memory": "Arbeitsspeicher", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "System auswählen", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "Nicht gefunden", "text": "Konnte die angeforderte Ressource nicht finden" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "Startseite", @@ -4499,86 +4555,14 @@ "disabled": "Deaktiviert" }, "job": { - "minecraftServerStatus": { - "label": "Status des Minecraft Servers" - }, - "timetable": { - "label": "" - }, "iconsUpdater": { "label": "Symbol Updater" }, "analytics": { "label": "Nutzungsdaten" }, - "smartHomeEntityState": { - "label": "Status der Smart Home Entitäten" - }, "ping": { "label": "Pings" - }, - "mediaServer": { - "label": "Medien Server" - }, - "mediaOrganizer": { - "label": "Medienorganisatoren" - }, - "downloads": { - "label": "Downloads" - }, - "mediaRequestStats": { - "label": "Statistik der Medienanfragen" - }, - "mediaRequestList": { - "label": "Liste der Medienanfragen" - }, - "rssFeeds": { - "label": "RSS Feeds" - }, - "indexerManager": { - "label": "Index Manager" - }, - "healthMonitoring": { - "label": "Überwachung des Zustandes" - }, - "dnsHole": { - "label": "DNS Hole Daten" - }, - "updateChecker": { - "label": "Updateprüfer" - }, - "mediaTranscoding": { - "label": "Medien Transkodierung" - }, - "networkController": { - "label": "Netzwerk Controller" - }, - "refreshNotifications": { - "label": "Benachrichtigungs Updater" - }, - "dockerContainers": { - "label": "Docker Container" - }, - "firewallCpu": { - "label": "Firewall CPU" - }, - "firewallMemory": { - "label": "Firewall Speicher" - }, - "firewallVersion": { - "label": "Firewall Version" - }, - "firewallInterfaces": { - "label": "Firewall Schnittstellen" - }, - "weather": { - "label": "Wetter" - }, - "tracearr": { - "label": "" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "Fehler beim Abrufen der Docker Container" + "internalServerError": "Fehler beim Abrufen der Docker Container", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/el.json b/packages/translation/src/lang/el.json index 168501668..3e64041dd 100644 --- a/packages/translation/src/lang/el.json +++ b/packages/translation/src/lang/el.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "", "speedtest": "", "analytics": "", + "vpn": "", "mediaLibrary": "", "uptimeMonitoring": "" }, @@ -1412,7 +1413,10 @@ "here": "εδώ", "iconPicker": { "label": "", - "header": "" + "header": "", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "Εναλλαγή σε σκοτεινή λειτουργία", "switchToLightMode": "Εναλλαγή σε φωτεινή λειτουργία", "management": "Διαχείριση", + "docker": "", "preferences": "Οι ρυθμίσεις σας", "logout": "Αποσύνδεση", "login": "Σύνδεση", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "Ρυθμίσεις" + "settings": "Ρυθμίσεις", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "", "noStation": "", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "Εμφάνιση Πληροφοριών Συστήματος Αρχείων" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -2352,6 +2368,9 @@ "name": "Στατιστικά Docker", "description": "", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "" }, @@ -2405,6 +2424,12 @@ "title": "" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "", "description": "" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "", @@ -3133,6 +3162,9 @@ "provider": { "label": "" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "", "placeholder": "" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "", "tooltip": "" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "", "noMatchingVersion": "", "noReleasesFound": "", - "noProviderSeleceted": "", - "noProviderResponse": "" + "noProviderSelected": "", + "noProviderResponse": "", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "", "text": "" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "Αρχική", @@ -4499,86 +4555,14 @@ "disabled": "" }, "job": { - "minecraftServerStatus": { - "label": "" - }, - "timetable": { - "label": "" - }, "iconsUpdater": { "label": "" }, "analytics": { "label": "" }, - "smartHomeEntityState": { - "label": "" - }, "ping": { "label": "" - }, - "mediaServer": { - "label": "Διακομιστής πολυμέσων" - }, - "mediaOrganizer": { - "label": "" - }, - "downloads": { - "label": "" - }, - "mediaRequestStats": { - "label": "" - }, - "mediaRequestList": { - "label": "" - }, - "rssFeeds": { - "label": "" - }, - "indexerManager": { - "label": "" - }, - "healthMonitoring": { - "label": "" - }, - "dnsHole": { - "label": "" - }, - "updateChecker": { - "label": "" - }, - "mediaTranscoding": { - "label": "" - }, - "networkController": { - "label": "" - }, - "refreshNotifications": { - "label": "" - }, - "dockerContainers": { - "label": "" - }, - "firewallCpu": { - "label": "" - }, - "firewallMemory": { - "label": "" - }, - "firewallVersion": { - "label": "" - }, - "firewallInterfaces": { - "label": "" - }, - "weather": { - "label": "" - }, - "tracearr": { - "label": "" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "" + "internalServerError": "", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/en-gb.json b/packages/translation/src/lang/en-gb.json index ff08eecf7..15ed8c7fe 100644 --- a/packages/translation/src/lang/en-gb.json +++ b/packages/translation/src/lang/en-gb.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "", "speedtest": "", "analytics": "", + "vpn": "", "mediaLibrary": "", "uptimeMonitoring": "" }, @@ -1412,7 +1413,10 @@ "here": "here", "iconPicker": { "label": "Icon URL", - "header": "Please enter a name or object to filter for the icon. Homarr will search throughout {countIcons} different icons to find the right one." + "header": "Please enter a name or object to filter for the icon. Homarr will search throughout {countIcons} different icons to find the right one.", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "Switch to dark mode", "switchToLightMode": "Switch to light mode", "management": "Management", + "docker": "", "preferences": "Your preferences", "logout": "Log-out", "login": "Log-in", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "Settings" + "settings": "Settings", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "Departures {station}", "noStation": "No station selected", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "Station" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "Show Filesystem Info" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Default tab" }, @@ -2352,6 +2368,9 @@ "name": "Docker stats", "description": "Stats of your containers (This widget can only be added with administrator privileges)", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "Enable items sorting" }, @@ -2405,6 +2424,12 @@ "title": "Instances" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "", "description": "" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "", @@ -3133,6 +3162,9 @@ "provider": { "label": "" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "", "placeholder": "" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "", "tooltip": "" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "", "noMatchingVersion": "", "noReleasesFound": "", - "noProviderSeleceted": "", - "noProviderResponse": "" + "noProviderSelected": "", + "noProviderResponse": "", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "", "text": "" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "", @@ -4499,86 +4555,14 @@ "disabled": "" }, "job": { - "minecraftServerStatus": { - "label": "" - }, - "timetable": { - "label": "" - }, "iconsUpdater": { "label": "" }, "analytics": { "label": "" }, - "smartHomeEntityState": { - "label": "" - }, "ping": { "label": "" - }, - "mediaServer": { - "label": "" - }, - "mediaOrganizer": { - "label": "" - }, - "downloads": { - "label": "" - }, - "mediaRequestStats": { - "label": "" - }, - "mediaRequestList": { - "label": "" - }, - "rssFeeds": { - "label": "" - }, - "indexerManager": { - "label": "" - }, - "healthMonitoring": { - "label": "" - }, - "dnsHole": { - "label": "" - }, - "updateChecker": { - "label": "" - }, - "mediaTranscoding": { - "label": "" - }, - "networkController": { - "label": "" - }, - "refreshNotifications": { - "label": "" - }, - "dockerContainers": { - "label": "" - }, - "firewallCpu": { - "label": "" - }, - "firewallMemory": { - "label": "" - }, - "firewallVersion": { - "label": "" - }, - "firewallInterfaces": { - "label": "" - }, - "weather": { - "label": "" - }, - "tracearr": { - "label": "" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "" + "internalServerError": "", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 21727fa3f..a2f2bed8a 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -1285,6 +1285,14 @@ "label": "API Key (Secret)", "newLabel": "New API Key (Secret)" }, + "patchmonApiKey": { + "label": "API Key", + "newLabel": "New API Key" + }, + "patchmonApiSecret": { + "label": "API Secret", + "newLabel": "New API Secret" + }, "githubAppId": { "label": "App Id", "newLabel": "New App Id" @@ -1328,9 +1336,21 @@ } } }, + "traefik": { + "name": "Traefik", + "description": "Cloud-native reverse proxy and load balancer" + }, "paperlessNgx": { "name": "Paperless-ngx", "description": "Document management system for archiving and organizing scanned documents" + }, + "patchmon": { + "name": "PatchMon", + "description": "Patch management and monitoring for Linux hosts" + }, + "bazarr": { + "name": "Bazarr", + "description": "Companion application to Sonarr and Radarr that manages and downloads subtitles" } }, "media": { @@ -1598,7 +1618,10 @@ }, "menu": { "label": { - "settings": "Settings" + "settings": "Settings", + "refresh": "Refresh", + "options": "Options", + "actions": "Actions" } }, "create": { @@ -1900,6 +1923,9 @@ "title": "Departures {station}", "noStation": "No station selected", "option": { + "baseUrl": { + "label": "Search.ch URL" + }, "station": { "label": "Station" } @@ -2278,6 +2304,11 @@ "fileSystem": { "label": "Show Filesystem Info" }, + "visibleStorageVolumes": { + "label": "Visible storage volumes", + "description": "Only available when all selected integrations are Synology. Leave empty to show all detected storage volumes.", + "placeholder": "Select storage volumes" + }, "defaultTab": { "label": "Default tab" }, @@ -2357,6 +2388,9 @@ "name": "Docker stats", "description": "Stats of your containers (This widget can only be added with administrator privileges)", "option": { + "columns": { + "label": "Columns to show" + }, "enableRowSorting": { "label": "Enable items sorting" }, @@ -2740,6 +2774,193 @@ "internalServerError": "Failed to fetch Paperless-ngx data" } }, + "patchmon": { + "name": "PatchMon", + "description": "Overview of host patch status from PatchMon", + "title": "PatchMon Statistics", + "totalHosts": "Total Hosts", + "hostsNeedingUpdates": "Needs Updates", + "securityUpdates": "Security Updates", + "upToDateHosts": "Up-to-Date Hosts", + "hostsWithSecurityUpdates": "Hosts with Security Updates", + "recentUpdates24h": "Updates (24h)", + "totalOutdatedPackages": "Outdated Packages", + "totalRepos": "Repositories", + "osDistribution": "OS Distribution", + "compliance": "Patch Compliance", + "complianceRatio": "{upToDate} / {total}", + "lastUpdated": "Updated {time}", + "option": { + "showTotalHosts": { + "label": "Show total hosts", + "description": "Display the total number of monitored hosts" + }, + "showHostsNeedingUpdates": { + "label": "Show hosts needing updates", + "description": "Display hosts with at least one outdated package" + }, + "showSecurityUpdates": { + "label": "Show security updates", + "description": "Display the total number of security updates available" + }, + "showUpToDateHosts": { + "label": "Show up-to-date hosts", + "description": "Display hosts with zero outdated packages" + }, + "showHostsWithSecurityUpdates": { + "label": "Show hosts with security updates", + "description": "Display hosts requiring security patches" + }, + "showRecentUpdates24h": { + "label": "Show recent updates (24h)", + "description": "Display the number of updates applied in the last 24 hours" + }, + "showTotalOutdatedPackages": { + "label": "Show outdated packages", + "description": "Display the total number of outdated packages across all hosts" + }, + "showTotalRepos": { + "label": "Show repositories", + "description": "Display the number of active repositories being monitored" + }, + "enableThresholdColors": { + "label": "Enable threshold colors", + "description": "Color stat values green, yellow, or red based on severity presets" + }, + "useCustomThresholds": { + "label": "Advanced thresholds", + "description": "Customize warning and critical thresholds per stat using absolute counts or percentages of total hosts" + }, + "thresholdMode": { + "option": { + "absolute": "Absolute count", + "percent": "Percent of total hosts" + } + }, + "hostsNeedingUpdatesThresholdMode": { + "label": "Needs updates threshold mode", + "description": "Compare the needs-updates count as an absolute value or percentage of total hosts" + }, + "hostsNeedingUpdatesWarningAt": { + "label": "Needs updates warning threshold", + "description": "Enter yellow when the value reaches this threshold" + }, + "hostsNeedingUpdatesCriticalAt": { + "label": "Needs updates critical threshold", + "description": "Enter red when the value reaches this threshold" + }, + "securityUpdatesThresholdMode": { + "label": "Security updates threshold mode", + "description": "Compare the security-updates count as an absolute value or percentage of total hosts" + }, + "securityUpdatesWarningAt": { + "label": "Security updates warning threshold", + "description": "Enter yellow when the value reaches this threshold" + }, + "securityUpdatesCriticalAt": { + "label": "Security updates critical threshold", + "description": "Enter red when the value reaches this threshold" + }, + "hostsWithSecurityUpdatesThresholdMode": { + "label": "Hosts with security updates threshold mode", + "description": "Compare the host count as an absolute value or percentage of total hosts" + }, + "hostsWithSecurityUpdatesWarningAt": { + "label": "Hosts with security updates warning threshold", + "description": "Enter yellow when the value reaches this threshold" + }, + "hostsWithSecurityUpdatesCriticalAt": { + "label": "Hosts with security updates critical threshold", + "description": "Enter red when the value reaches this threshold" + }, + "upToDateHostsThresholdMode": { + "label": "Up-to-date hosts threshold mode", + "description": "Compare the up-to-date host count as an absolute value or percentage of total hosts" + }, + "upToDateHostsWarningAt": { + "label": "Up-to-date hosts warning threshold", + "description": "Enter yellow when the value is at or above this threshold but below full coverage" + }, + "upToDateHostsCriticalAt": { + "label": "Up-to-date hosts critical threshold", + "description": "Enter red when the value falls below this threshold" + }, + "totalOutdatedPackagesThresholdMode": { + "label": "Outdated packages threshold mode", + "description": "Compare the outdated-packages count as an absolute value or percentage of total hosts" + }, + "totalOutdatedPackagesWarningAt": { + "label": "Outdated packages warning threshold", + "description": "Enter yellow when the value reaches this threshold" + }, + "totalOutdatedPackagesCriticalAt": { + "label": "Outdated packages critical threshold", + "description": "Enter red when the value reaches this threshold" + }, + "showOsDistribution": { + "label": "Show OS distribution", + "description": "Display a breakdown of monitored hosts by operating system" + }, + "osDistributionLimit": { + "label": "OS distribution limit", + "description": "Maximum number of operating systems to display", + "option": { + "3": "Top 3", + "5": "Top 5", + "10": "Top 10", + "all": "All" + } + }, + "showOsVersion": { + "label": "Show OS version", + "description": "Append the OS version to each distribution label when available" + }, + "showComplianceHero": { + "label": "Show compliance hero", + "description": "Display a compliance ring showing the percentage of up-to-date hosts" + }, + "osDisplayMode": { + "label": "OS display mode", + "description": "Choose how to display the operating system distribution", + "option": { + "bars": "Horizontal bars", + "donut": "Donut chart" + } + } + }, + "error": { + "internalServerError": "Failed to fetch PatchMon data" + } + }, + "bazarr": { + "name": "Bazarr", + "description": "Overview of missing subtitles and health status from your Bazarr instance", + "episodes": "Missing Episodes", + "movies": "Missing Movies", + "providers": "Provider Issues", + "status": "Health Issues", + "option": { + "showMissingEpisodes": { + "label": "Show missing episodes", + "description": "Display the number of episodes with missing subtitles" + }, + "showMissingMovies": { + "label": "Show missing movies", + "description": "Display the number of movies with missing subtitles" + }, + "showProviderIssues": { + "label": "Show provider issues", + "description": "Display the number of subtitle providers with issues" + }, + "showHealthIssues": { + "label": "Show health issues", + "description": "Display the number of Bazarr health issues" + } + }, + "error": { + "internalServerError": "Failed to fetch Bazarr data" + } + }, "immich-serverStats": { "name": "Immich Server Stats", "description": "Information about your Immich instance", @@ -2770,8 +2991,8 @@ "name": "Immich Album", "option": { "albumId": { - "label": "Album ID", - "description": "This is a unique ID which refers to your Immich album. You can find this id by opening an album and copying the unique string from the URL, for example 'a21aca09-9f05-47dc-b1a1-7b53c547226e'" + "label": "Album", + "description": "Select the Immich album to show in the slideshow." }, "rotationIntervalSeconds": { "label": "Interval in seconds", @@ -2780,6 +3001,10 @@ "showPhotoInfo": { "label": "Show photo info", "description": "Display date of the picture and the current index at the bottom of the slideshow" + }, + "randomizePhotos": { + "label": "Randomize photos", + "description": "Show photos in a random order instead of always starting with the same album order." } }, "description": "Slideshow of pictures in your Immich album", @@ -3075,6 +3300,35 @@ } } }, + "mediaMissing": { + "name": "Missing & Queued Media", + "description": "Shows missing and queued movies and episodes from Radarr and Sonarr", + "option": { + "showMissing": { + "label": "Show missing items" + }, + "showQueued": { + "label": "Show queued items" + }, + "pageSize": { + "label": "Items per page" + } + }, + "tab": { + "missing": "Missing", + "queued": "Queued" + }, + "type": { + "movie": "Movie", + "episode": "Episode" + }, + "empty": { + "missing": "No missing items", + "queued": "No queued items", + "noTabsEnabled": "Enable at least one tab in the widget settings" + }, + "timeLeft": "Time left" + }, "rssFeed": { "name": "RSS feeds", "description": "Monitor and display one or more generic RSS, ATOM or JSON feeds", @@ -3144,6 +3398,9 @@ "provider": { "label": "Provider" }, + "providerUrl": { + "label": "Provider URL" + }, "identifier": { "label": "Identifier", "placeholder": "Name or Owner/Name" @@ -3191,6 +3448,13 @@ "noProvider": { "label": "No Provider", "tooltip": "The provider could not be parsed, please manually set it after importing the images" + }, + "tokens": { + "label": "Provider Tokens", + "token": "Token", + "configured": "Token configured", + "notConfigured": "No token set", + "save": "Save" } } }, @@ -3213,8 +3477,9 @@ "invalidIdentifier": "Invalid identifier", "noMatchingVersion": "No matching version found", "noReleasesFound": "No releases found", - "noProviderSeleceted": "No provider selected", - "noProviderResponse": "No response from provider" + "noProviderSelected": "No provider selected", + "noProviderResponse": "No response from provider", + "unexpected": "Unexpected provider error" } } }, @@ -3307,7 +3572,11 @@ "name": "Notifications", "description": "Display notification history from an integration", "noItems": "No notifications to display.", - "option": {} + "option": { + "hideLogos": { + "label": "Hide logos" + } + } }, "speedtestTracker": { "name": "Speedtest Tracker", @@ -3388,6 +3657,40 @@ "internalServerError": "Failed to fetch uptime data" } }, + "traefik": { + "name": "Traefik", + "description": "Overview of Traefik routers, services, middlewares, and entry points", + "option": { + "showTcp": { + "label": "Show TCP" + }, + "showUdp": { + "label": "Show UDP" + }, + "showEntryPoints": { + "label": "Show entry points" + } + }, + "instances": "Traefik instances", + "versionUnknown": "Version unknown", + "summary": { + "routers": "Routers", + "entryPoints": "Entry points" + }, + "resource": { + "routers": "Routers", + "services": "Services", + "middlewares": "Middlewares" + }, + "status": { + "healthy": "Healthy", + "errors": "{count} errors", + "warnings": "{count} warnings" + }, + "error": { + "internalServerError": "Failed to fetch Traefik data" + } + }, "audioStats": { "name": "Audio stats", "description": "Library and playback statistics from Navidrome or Audiobookshelf", @@ -3514,6 +3817,11 @@ }, "showBackgroundBar": { "label": "Show background usage bar" + }, + "visibleStorageVolumes": { + "label": "Visible storage volumes", + "description": "Only available when all selected integrations are Synology. Leave empty to show all detected storage volumes.", + "placeholder": "Select storage volumes" } }, "status": { @@ -3803,8 +4111,8 @@ }, "disk": { "title": "Disk Usage", - "subtitle": "Usage of root partition", - "series": "Used" + "subtitle": "Usage by filesystem", + "series": "Root" }, "diskIO": { "title": "Disk I/O", @@ -3812,6 +4120,11 @@ "read": "Read", "write": "Write" }, + "storage": { + "title": "Storage", + "subtitle": "Per-filesystem disk usage", + "series": "Used" + }, "network": { "title": "Bandwidth", "subtitle": "Network traffic of public interfaces", @@ -4362,7 +4675,7 @@ "notStarted": "Not started" }, "name": { -"management": "Management", + "management": "Management", "dashboard": "Dashboard" } } @@ -4575,89 +4888,14 @@ "disabled": "Disabled" }, "job": { - "minecraftServerStatus": { - "label": "Minecraft server status" - }, - "timetable": { - "label": "Timetable" - }, "iconsUpdater": { "label": "Icons Updater" }, "analytics": { "label": "Analytics" }, - "smartHomeEntityState": { - "label": "Smart Home Entity State" - }, "ping": { "label": "Pings" - }, - "mediaServer": { - "label": "Media Server" - }, - "mediaOrganizer": { - "label": "Media Organizers" - }, - "downloads": { - "label": "Downloads" - }, - "mediaRequestStats": { - "label": "Media Request Stats" - }, - "mediaRequestList": { - "label": "Media Request List" - }, - "rssFeeds": { - "label": "RSS feeds" - }, - "indexerManager": { - "label": "Indexer Manager" - }, - "healthMonitoring": { - "label": "Health Monitoring" - }, - "dnsHole": { - "label": "DNS Hole Data" - }, - "updateChecker": { - "label": "Update checker" - }, - "mediaTranscoding": { - "label": "Media transcoding" - }, - "networkController": { - "label": "Network Controller" - }, - "refreshNotifications": { - "label": "Notification Updater" - }, - "dockerContainers": { - "label": "Docker containers" - }, - "firewallCpu": { - "label": "Firewall CPU" - }, - "firewallMemory": { - "label": "Firewall Memory" - }, - "firewallVersion": { - "label": "Firewall Version" - }, - "firewallInterfaces": { - "label": "Firewall Interfaces" - }, - "weather": { - "label": "Weather" - }, - "tracearr": { - "label": "Tracearr" - }, - "umamiActiveVisitors": { - "label": "Umami Active Visitors" - }, - "vpn": { - "label": "VPN Status" } }, "interval": { @@ -5150,7 +5388,8 @@ } }, "error": { - "internalServerError": "Failed to fetch Docker containers" + "internalServerError": "Failed to fetch Docker containers", + "noColumns": "Select at least one column in the widget settings" } }, "kubernetes": { diff --git a/packages/translation/src/lang/es.json b/packages/translation/src/lang/es.json index 7858cc262..a64ae6915 100644 --- a/packages/translation/src/lang/es.json +++ b/packages/translation/src/lang/es.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "", "speedtest": "", "analytics": "", + "vpn": "", "mediaLibrary": "", "uptimeMonitoring": "" }, @@ -1412,7 +1413,10 @@ "here": "aquí", "iconPicker": { "label": "URL del icono", - "header": "Escribe nombre u objetos para filtrar iconos... Homarr buscará a través de iconos {countIcons} por ti." + "header": "Escribe nombre u objetos para filtrar iconos... Homarr buscará a través de iconos {countIcons} por ti.", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "Cambiar a modo oscuro", "switchToLightMode": "Cambiar a modo claro", "management": "Administración", + "docker": "", "preferences": "Tus preferencias", "logout": "Cerrar sesión", "login": "Iniciar sesión", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "Ajustes" + "settings": "Ajustes", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "Salidas {station}", "noStation": "No se ha seleccionado ninguna integración", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "Estación" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "Mostrar información del sistema de archivos" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Seleccionar pestaña predeterminada" }, @@ -2352,6 +2368,9 @@ "name": "Estadísticas de Docker", "description": "Estadísticas de tus contenedores (Este widget sólo puede ser añadido con privilegios de administrador)", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "Habilitar ordenar elementos" }, @@ -2405,6 +2424,12 @@ "title": "Instancias" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "Mostrar información de la foto", "description": "Mostrar la fecha y el índice actual de la foto en la parte inferior de la presentación" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "Presentación de fotos de un álbum de Immich", @@ -3133,6 +3162,9 @@ "provider": { "label": "Proveedor" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "Identificador", "placeholder": "Nombre o Propietario/Nombre" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "Sin Proveedor", "tooltip": "El proveedor no pudo ser analizado, por favor configurarlo manualmente después de importar las imágenes" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "Identificador no válido", "noMatchingVersion": "No se encontró una versión correspondiente", "noReleasesFound": "No se encontraron publicaciones", - "noProviderSeleceted": "No se ha seleccionado ningún proveedor", - "noProviderResponse": "No hay respuesta del proveedor" + "noProviderSelected": "", + "noProviderResponse": "No hay respuesta del proveedor", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "Mostrar la barra de uso en segundo plano" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "No Encontrado", "text": "No se pudo encontrar el recurso solicitado" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "Inicio", @@ -4499,86 +4555,14 @@ "disabled": "Deshabilitado" }, "job": { - "minecraftServerStatus": { - "label": "Estado del Servidor de Minecraft" - }, - "timetable": { - "label": "Horarios de estación" - }, "iconsUpdater": { "label": "Actualizador de iconos" }, "analytics": { "label": "Estadísticas" }, - "smartHomeEntityState": { - "label": "Estado de Entidad de Smart Home" - }, "ping": { "label": "Pings" - }, - "mediaServer": { - "label": "Servidor Multimedia" - }, - "mediaOrganizer": { - "label": "Organizadores multimedia" - }, - "downloads": { - "label": "Descargas" - }, - "mediaRequestStats": { - "label": "Estadísticas de solicitudes de medios" - }, - "mediaRequestList": { - "label": "Lista de solicitudes de medios" - }, - "rssFeeds": { - "label": "Fuentes RSS" - }, - "indexerManager": { - "label": "Administrador de Índices" - }, - "healthMonitoring": { - "label": "Monitoreo de Salud" - }, - "dnsHole": { - "label": "Datos de agujero DNS" - }, - "updateChecker": { - "label": "Comprobador de actualización" - }, - "mediaTranscoding": { - "label": "Transcodificación de medios" - }, - "networkController": { - "label": "Controlador de red" - }, - "refreshNotifications": { - "label": "Actualizador de notificaciones" - }, - "dockerContainers": { - "label": "Contenedores de Docker" - }, - "firewallCpu": { - "label": "CPU del Firewall" - }, - "firewallMemory": { - "label": "Memoria del Firewall" - }, - "firewallVersion": { - "label": "Versión del Firewall" - }, - "firewallInterfaces": { - "label": "Interfaces del Firewall" - }, - "weather": { - "label": "Tiempo" - }, - "tracearr": { - "label": "Tracearr" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "Fallo al obtener los contenedores de Docker" + "internalServerError": "Fallo al obtener los contenedores de Docker", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/et.json b/packages/translation/src/lang/et.json index 606d4833f..23da3299c 100644 --- a/packages/translation/src/lang/et.json +++ b/packages/translation/src/lang/et.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "", "speedtest": "", "analytics": "", + "vpn": "", "mediaLibrary": "", "uptimeMonitoring": "" }, @@ -1412,7 +1413,10 @@ "here": "", "iconPicker": { "label": "", - "header": "" + "header": "", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "", "switchToLightMode": "", "management": "", + "docker": "", "preferences": "Teie eelistused", "logout": "", "login": "", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "" + "settings": "", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "", "noStation": "", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -2352,6 +2368,9 @@ "name": "", "description": "", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "" }, @@ -2405,6 +2424,12 @@ "title": "" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "", "description": "" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "", @@ -3133,6 +3162,9 @@ "provider": { "label": "" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "", "placeholder": "" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "", "tooltip": "" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "", "noMatchingVersion": "", "noReleasesFound": "", - "noProviderSeleceted": "", - "noProviderResponse": "" + "noProviderSelected": "", + "noProviderResponse": "", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "", "text": "" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "", @@ -4499,86 +4555,14 @@ "disabled": "" }, "job": { - "minecraftServerStatus": { - "label": "" - }, - "timetable": { - "label": "" - }, "iconsUpdater": { "label": "" }, "analytics": { "label": "" }, - "smartHomeEntityState": { - "label": "" - }, "ping": { "label": "" - }, - "mediaServer": { - "label": "" - }, - "mediaOrganizer": { - "label": "" - }, - "downloads": { - "label": "" - }, - "mediaRequestStats": { - "label": "" - }, - "mediaRequestList": { - "label": "" - }, - "rssFeeds": { - "label": "" - }, - "indexerManager": { - "label": "" - }, - "healthMonitoring": { - "label": "" - }, - "dnsHole": { - "label": "" - }, - "updateChecker": { - "label": "" - }, - "mediaTranscoding": { - "label": "" - }, - "networkController": { - "label": "" - }, - "refreshNotifications": { - "label": "" - }, - "dockerContainers": { - "label": "" - }, - "firewallCpu": { - "label": "" - }, - "firewallMemory": { - "label": "" - }, - "firewallVersion": { - "label": "" - }, - "firewallInterfaces": { - "label": "" - }, - "weather": { - "label": "" - }, - "tracearr": { - "label": "" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "" + "internalServerError": "", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/fi.json b/packages/translation/src/lang/fi.json index 713c729b1..7388d9322 100644 --- a/packages/translation/src/lang/fi.json +++ b/packages/translation/src/lang/fi.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "Valvonta", "speedtest": "Nopeustestaus", "analytics": "Analytiikka", + "vpn": "", "mediaLibrary": "", "uptimeMonitoring": "" }, @@ -1044,7 +1045,7 @@ "description": "Vastausta ei voitu jäsentää. Tarkista, että URL-osoite osoittaa palvelun perusosoitteeseen." }, "authorization": { - "title": "Tuonnistautumisvirhe", + "title": "Tunnistautumisvirhe", "description": "Pyyntöä ei hyväksytty. Varmista, että kirjautumistiedot ovat oikein ja että niille on määritetty riittävät käyttöoikeudet." }, "statusCode": { @@ -1412,7 +1413,10 @@ "here": "", "iconPicker": { "label": "Kuvakkeen URL-osoite", - "header": "Suodata kuvakkeita syöttämällä nimi tai objekti... Homarr etsii {countIcons} kuvakkeesta." + "header": "Suodata kuvakkeita syöttämällä nimi tai objekti... Homarr etsii {countIcons} kuvakkeesta.", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "", "switchToLightMode": "", "management": "Hallinta", + "docker": "", "preferences": "Asetuksesi", "logout": "Kirjaudu ulos", "login": "", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "Asetukset" + "settings": "Asetukset", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "", "noStation": "", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -2352,6 +2368,9 @@ "name": "Docker-tilastot", "description": "Konttiesi tilastot (tämä widget voidaan lisätä vain ylläpito-oikeuksilla)", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "Salli kohteiden järjestys" }, @@ -2405,6 +2424,12 @@ "title": "" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "", "description": "" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "", @@ -3133,6 +3162,9 @@ "provider": { "label": "" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "", "placeholder": "" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "", "tooltip": "" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "", "noMatchingVersion": "", "noReleasesFound": "", - "noProviderSeleceted": "Toimittajaa ei ole valittu", - "noProviderResponse": "" + "noProviderSelected": "", + "noProviderResponse": "", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "", "text": "" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "Aloitus", @@ -4499,86 +4555,14 @@ "disabled": "Ei käytössä" }, "job": { - "minecraftServerStatus": { - "label": "Minecraft-palvelimen tila" - }, - "timetable": { - "label": "Aikataulu" - }, "iconsUpdater": { "label": "Kuvakepäivitykset" }, "analytics": { "label": "Analytiikka" }, - "smartHomeEntityState": { - "label": "Älykodin entiteettitila" - }, "ping": { "label": "Ping-koestukset" - }, - "mediaServer": { - "label": "Mediapalvelimet" - }, - "mediaOrganizer": { - "label": "Median hallintatyökalut" - }, - "downloads": { - "label": "Lataukset" - }, - "mediaRequestStats": { - "label": "Mediapyyntöjen tilastot" - }, - "mediaRequestList": { - "label": "Mediapyyntöjen listaus" - }, - "rssFeeds": { - "label": "RSS-syötteet" - }, - "indexerManager": { - "label": "" - }, - "healthMonitoring": { - "label": "Terveydentilan valvonta" - }, - "dnsHole": { - "label": "" - }, - "updateChecker": { - "label": "Päivitystarkistus" - }, - "mediaTranscoding": { - "label": "Median transkoodaus" - }, - "networkController": { - "label": "" - }, - "refreshNotifications": { - "label": "Ilmoituspäivitykset" - }, - "dockerContainers": { - "label": "Docker-kontit" - }, - "firewallCpu": { - "label": "Palomuurin suoritin" - }, - "firewallMemory": { - "label": "Palomuurin muisti" - }, - "firewallVersion": { - "label": "Palomuurin versio" - }, - "firewallInterfaces": { - "label": "" - }, - "weather": { - "label": "Sää" - }, - "tracearr": { - "label": "Tracearr" - }, - "umamiActiveVisitors": { - "label": "Umamin aktiiviset käyttäjät" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "" + "internalServerError": "", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/fr.json b/packages/translation/src/lang/fr.json index 25b85fe71..881db7cee 100644 --- a/packages/translation/src/lang/fr.json +++ b/packages/translation/src/lang/fr.json @@ -8,9 +8,9 @@ "action": { "scratch": "Partir de zéro", "importOldmarr": "Importer depuis Homarr avant la 1.0", - "restoreSqlite": "", - "restoreSqliteDescription": "", - "backToOptions": "" + "restoreSqlite": "Restaurer depuis une sauvegarde SQLite", + "restoreSqliteDescription": "Téléchargez une sauvegarde complète de la base de données SQLite depuis une instance Homarr précédente. Cela restaurera tous les tableaux, utilisateurs, applications, intégrations et paramètres.", + "backToOptions": "← Retour aux options de configuration" } }, "import": { @@ -110,29 +110,29 @@ "message": "Une erreur s'est produite lors de la configuration de votre tableau de bord. Veuillez réessayer." }, "helpers": { - "tooltip": "", - "description": "" + "tooltip": "Aides pour les URL", + "description": "Ces aides essaient de deviner l'URL de vos services. Si cela ne fonctionne pas, tapez simplement l'adresse manuellement." } }, "urlTemplate": { - "label": "", - "placeholder": "", - "hint": "", - "warning": "", + "label": "URL de base", + "placeholder": "homelab.local ou 192.168.1.1", + "hint": "Entrez votre domaine de base ou votre IP pour le remplissage automatique des URL d'intégration", + "warning": "Définir votre URL de base maintenant remplira automatiquement toutes les URL d'intégration, ce qui vous fera gagner du temps lors de la configuration", "preview": "ex. {example}", "mode": { "subdomain": "Sous-domaine", - "hostPort": "" + "hostPort": "Hôte:Port" } }, "docker": { - "detected": "", - "detectedBreakdown": "", - "summary": "", - "appBadge": "", - "appSectionLabel": "", - "scanning": "", - "scanningDescription": "" + "detected": "Docker a détecté {count} services", + "detectedBreakdown": "Intégrations {integrations} · Applications {apps}", + "summary": "Les applications {apps} ont été détectées par Homarr, dont {integrations} ont des intégrations intégrées", + "appBadge": "App", + "appSectionLabel": "Applications à ajouter au tableau de bord ({count} sélectionné)", + "scanning": "Analyse de Docker...", + "scanningDescription": "Recherche de conteneurs en cours d'exécution sur votre système" } }, "finish": { @@ -177,7 +177,7 @@ }, "password": { "label": "Mot de passe", - "suggestionsTitle": "", + "suggestionsTitle": "Suggestions de mot de passe", "suggestion": { "length": "Contient au moins 8 caractères", "lowercase": "Inclut une lettre minuscule", @@ -205,7 +205,7 @@ "label": "Ouvrir les résultats de recherche dans un nouvel onglet" }, "ddgBangs": { - "label": "" + "label": "Activer les bangs DuckDuckGo" } }, "error": { @@ -298,10 +298,10 @@ "changeDdgBangs": { "notification": { "success": { - "message": "" + "message": "Bangs DuckDuckGo activés avec succès" }, "error": { - "message": "" + "message": "Impossible d'activer/désactiver les bangs DuckDuckGo" } } }, @@ -687,53 +687,53 @@ "customWidget": { "page": { "list": { - "title": "", - "noResults": "", - "loading": "" + "title": "Widgets personnalisés", + "noResults": "Pas encore de widgets personnalisés. Créez votre premier widget!", + "loading": "Chargement..." }, "create": { - "title": "", - "subtitle": "" + "title": "Créer un widget personnalisé", + "subtitle": "Définissez une URL de l'API, l'authentification, et comment afficher les données de réponse." }, "edit": { - "title": "" + "title": "Modifier le widget personnalisé" } }, "action": { - "create": "", - "import": "", - "save": "", - "delete": "", - "deleteConfirm": "", - "export": "", + "create": "Créer", + "import": "Importer", + "save": "Sauvegarder", + "delete": "Supprimer le widget personnalisé", + "deleteConfirm": "Êtes-vous sûr de vouloir supprimer \"{name}\" ? Cette action est irréversible.", + "export": "Exporter", "addMapping": "", - "addColumn": "", - "edit": "", + "addColumn": "Ajouter une colonne", + "edit": "Modifier", "removeMapping": "", - "removeColumn": "", - "addItem": "", - "removeItem": "", - "addBar": "", - "duplicate": "", - "copyAiPrompt": "", - "enable": "", - "disable": "", - "menu": "" + "removeColumn": "Supprimer la colonne", + "addItem": "Ajouter un élément", + "removeItem": "Supprimer l'élément", + "addBar": "Ajouter une barre", + "duplicate": "Dupliquer", + "copyAiPrompt": "Copier le prompt IA", + "enable": "Activer", + "disable": "Désactiver", + "menu": "Actions" }, "field": { - "name": "", - "description": "", - "iconUrl": "", - "url": "", - "authType": "", + "name": "Nom", + "description": "Description", + "iconUrl": "URL de l'icône", + "url": "URL", + "authType": "Type d’authentification", "headerName": "", "method": "", "requestBody": "", "displayType": "", "jsonPath": "", "jsonPathHint": "", - "label": "", - "unit": "", + "label": "Étiquette", + "unit": "Unité", "mappings": "", "tablePath": "", "tablePathHint": "", @@ -753,28 +753,28 @@ "labelPosition": "", "kvLayout": "", "striped": "", - "compact": "", + "compact": "Mode Compact", "maxHeight": "", - "color": "", + "color": "Couleur", "buttonLabel": "", "buttonColor": "", - "confirmText": "", + "confirmText": "Texte de confirmation", "confirmTextHint": "", - "successMessage": "", + "successMessage": "Message de réussite", "successMessageHint": "", - "enabled": "", + "enabled": "Activé", "template": { - "label": "", + "label": "Template JSX", "description": "" } }, "placeholder": { "exampleLabel": "", - "exampleLabelShort": "", - "exampleHeader": "", - "exampleUnit": "", - "iconUrl": "", - "url": "", + "exampleLabelShort": "Température du CPU", + "exampleHeader": "Nom", + "exampleUnit": "°C", + "iconUrl": "https://...", + "url": "https://api.example.com/api/v1/status", "headerName": "", "jsonPath": "", "tablePath": "", @@ -797,27 +797,27 @@ "sm": "", "md": "", "lg": "", - "xl": "" + "xl": "Très grand" }, "labelPositionOption": { - "above": "", - "below": "" + "above": "Au-dessus", + "below": "En dessous" }, "fieldset": { - "general": "", - "connection": "", - "display": "" + "general": "Général", + "connection": "Connexion API", + "display": "Configuration de l'affichage" }, "displayTypeDescription": { - "singleValue": "", - "keyValue": "", + "singleValue": "Afficher une valeur extraite avec une étiquette et une unité", + "keyValue": "Afficher plusieurs valeurs étiquetées côte à côte", "table": "", "statGrid": "", "progressBars": "", - "statusIndicator": "", + "statusIndicator": "Points d'état colorés montrant en ligne/hors ligne ou l'état de santé", "countGrid": "", "raw": "", - "actionButton": "", + "actionButton": "Un bouton qui déclenche une requête lorsqu'elle est cliquée", "customJsx": "" }, "method": { @@ -848,8 +848,8 @@ }, "secret": { "apiKey": "", - "username": "", - "password": "", + "username": "Nom d'utilisateur", + "password": "Mot de passe", "savedPlaceholder": "" }, "preview": { @@ -859,34 +859,34 @@ "rawResponse": "" }, "notification": { - "created": "", - "updated": "", - "deleted": "", - "imported": "", + "created": "Widget \"{name}\" créé avec succès.", + "updated": "Widget \"{name}\" mis à jour avec succès.", + "deleted": "Widget \"{name}\" supprimé.", + "imported": "Widget importé avec succès.", "aiPromptCopied": "", "aiPromptDescription": "", - "aiPromptCopy": "", + "aiPromptCopy": "Copier dans le presse-papiers", "aiPromptNoResponse": "", - "aiPromptCopyError": "", - "createError": "", - "updateError": "", - "importError": "", - "exportError": "", - "duplicated": "", - "duplicateError": "", - "toggleError": "", - "deleteError": "", - "previewError": "", - "executed": "", - "executeError": "" + "aiPromptCopyError": "Échec de la copie du prompt dans le presse-papiers. Votre navigateur peut ne pas prendre en charge cette fonctionnalité.", + "createError": "Impossible de créer le widget. Vérifiez que votre point de terminaison API est joignable.", + "updateError": "Impossible de mettre à jour le widget. Vos modifications n'ont pas été enregistrées.", + "importError": "Impossible d'importer le widget. Assurez-vous que le fichier est un export JSON de widget personnalisé valide.", + "exportError": "Impossible d'exporter le widget.", + "duplicated": "Widget \"{name}\" dupliqué.", + "duplicateError": "Impossible de dupliquer le widget.", + "toggleError": "Impossible de changer le statut du widget.", + "deleteError": "Impossible de supprimer le widget.", + "previewError": "Impossible de récupérer l'aperçu. Vérifiez l'URL et réessayez.", + "executed": "Action exécutée avec succès.", + "executeError": "Échec de l'action: {error}" }, "table": { - "name": "", - "url": "", - "display": "" + "name": "Nom", + "url": "URL", + "display": "Affichage" }, "widget": { - "disabled": "" + "disabled": "Ce widget est désactivé" } }, "integration": { @@ -1007,22 +1007,23 @@ "miscellaneous": "", "smartHomeServer": "", "indexerManager": "", - "healthMonitoring": "", - "search": "", - "mediaTranscoding": "", - "networkController": "", - "releasesProvider": "", - "notifications": "", - "firewall": "", + "healthMonitoring": "Santé", + "search": "Rechercher", + "mediaTranscoding": "Transcodage", + "networkController": "Réseau", + "releasesProvider": "Versions", + "notifications": "Notifications", + "firewall": "Pare-feu", "timetable": "", - "photoService": "", - "documents": "", - "notes": "", - "mediaMonitoring": "", - "speedtest": "", - "analytics": "", - "mediaLibrary": "", - "uptimeMonitoring": "" + "photoService": "Photos", + "documents": "Documents", + "notes": "Notes", + "mediaMonitoring": "Supervision", + "speedtest": "Test de débit", + "analytics": "Statistiques", + "vpn": "VPN", + "mediaLibrary": "Bibliothèque de médias", + "uptimeMonitoring": "Surveillance des disponibilités" }, "testConnection": { "action": { @@ -1297,8 +1298,8 @@ "newLabel": "Nouvelle clé privée" }, "slug": { - "label": "", - "newLabel": "" + "label": "Slug de la page de statut", + "newLabel": "Nouveau slug de page de statut" } } }, @@ -1312,7 +1313,7 @@ "description": "" }, "navidrome": { - "name": "", + "name": "Navidrome", "description": "" }, "uptimeKuma": { @@ -1384,7 +1385,7 @@ "add": "Ajouter", "apply": "Appliquer", "backToOverview": "Retourner à l'aperçu", - "skip": "", + "skip": "Ignorer", "change": "Modifier", "create": "Créer", "createAnother": "Créer et recommencer", @@ -1406,17 +1407,20 @@ "checkLogs": "Vérifiez les logs pour plus de détails", "tryAgain": "Réessayer", "loading": "Chargement", - "show": "", - "hide": "" + "show": "Afficher", + "hide": "Masquer" }, "here": "ici", "iconPicker": { "label": "URL de l'icône :", - "header": "Tapez le nom ou les objets pour filtrer les icônes... Homarr recherchera les icônes {countIcons} pour vous." + "header": "Tapez le nom ou les objets pour filtrer les icônes... Homarr recherchera les icônes {countIcons} pour vous.", + "headerLoading": "Tapez pour rechercher les icônes…", + "noResults": "Aucune icône ne correspond à \"{search}\".", + "error": "Impossible de charger les icônes." }, "colorScheme": { "options": { - "auto": "", + "auto": "Auto", "light": "Clair", "dark": "Sombre" } @@ -1464,6 +1468,7 @@ "switchToDarkMode": "Activer le mode sombre", "switchToLightMode": "Activer le mode clair", "management": "Administration", + "docker": "Docker", "preferences": "Vos préférences", "logout": "Se déconnecter", "login": "Connexion", @@ -1573,7 +1578,7 @@ "label": { "create": "Nouvelle catégorie", "changePosition": "Modifier la position", - "settings": "" + "settings": "Réglages" } }, "openAllInNewTabs": { @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "Paramètres" + "settings": "Paramètres", + "refresh": "Actualiser", + "options": "Options", + "actions": "Actions" } }, "create": { @@ -1621,11 +1629,11 @@ "edit": { "title": "Éditer l'élément", "tab": { - "widget": "", - "app": "" + "widget": "Éditer le widget", + "app": "Modifier l'application" }, "app": { - "propagationNotice": "" + "propagationNotice": "Les modifications apportées à cette application seront appliquées partout où elle est utilisée sur tous les tableaux de bord." }, "advancedOptions": { "label": "Options avancées", @@ -1653,11 +1661,11 @@ }, "widget": { "integrationSelect": { - "configureIntegrationFirst": "", - "loading": "", - "placeholder": "", - "noResults": "", - "loadError": "" + "configureIntegrationFirst": "Connectez d'abord une intégration", + "loading": "Chargement...", + "placeholder": "Choisir une option", + "noResults": "Aucun résultat trouvé", + "loadError": "Impossible de charger les options" }, "app": { "name": "App", @@ -1738,7 +1746,7 @@ "label": "Ouvrir dans un nouvel onglet" }, "withBorder": { - "label": "" + "label": "Afficher la bordure" }, "items": { "label": "Signets", @@ -1751,7 +1759,7 @@ "description": "", "option": { "showBroadcastMessage": { - "label": "" + "label": "Afficher les messages de diffusion" } } }, @@ -1891,10 +1899,13 @@ }, "timetable": { "name": "", - "description": "", + "description": "Affiche les heures de départ pour une station", "title": "", "noStation": "", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "" } @@ -1986,17 +1997,17 @@ }, "empty": "", "emptyContent": "", - "notFound": "", + "notFound": "Note introuvable", "noNotes": "", "integrationRequired": "", "updatedAt": "", "untitled": "", - "edit": "", - "save": "", - "cancel": "", + "edit": "Modifier", + "save": "Sauvegarder", + "cancel": "Annuler", "readOnlyViewer": "", - "saveForbidden": "", - "saveFailed": "" + "saveForbidden": "Vous n'êtes pas autorisé à modifier cette note.", + "saveFailed": "Impossible d'enregistrer cette note. Veuillez réessayer." }, "iframe": { "name": "iFrame", @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "Afficher les infos sur le système de fichiers" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Onglet par défaut" }, @@ -2352,6 +2368,9 @@ "name": "Statistiques de Docker", "description": "Statistiques de vos conteneurs (Ce widget ne peut être ajouté qu'avec les privilèges d'administrateur)", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "Activer le tri des éléments" }, @@ -2405,32 +2424,38 @@ "title": "Instances" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", "option": { "showBattery": { - "label": "" + "label": "Afficher la batterie" }, "showLoad": { - "label": "" + "label": "Afficher la charge" }, "showVoltage": { - "label": "" + "label": "Afficher la tension" } }, "status": { - "online": "", - "onBattery": "", - "lowBattery": "", - "charging": "", - "unknown": "" + "online": "En ligne", + "onBattery": "Sur batterie", + "lowBattery": "Batterie faible", + "charging": "Chargement", + "unknown": "Inconnu" }, "field": { - "runtime": "", - "load": "", - "input": "", - "output": "" + "runtime": "Runtime", + "load": "Charger", + "input": "Entrée", + "output": "Sortie" }, "error": { "internalServerError": "" @@ -2441,7 +2466,7 @@ "description": "Surveillance avancée du serveur multimédia avec des données détaillées de flux et de transcodage", "option": { "showStreams": { - "label": "" + "label": "Afficher les flux actifs" }, "showStats": { "label": "Afficher le résumé des statistiques" @@ -2454,13 +2479,13 @@ } }, "stats": { - "activeStreams": "", + "activeStreams": "Flux actif", "totalUsers": "Utilisateurs totaux", - "transcodes": "", + "transcodes": "Transcodages", "bandwidth": "Bande passante" }, "streams": { - "empty": "", + "empty": "Aucun flux actif", "user": "Utilisateur", "media": "Média", "state": "État", @@ -2502,32 +2527,32 @@ "avgDuration": "", "option": { "websiteId": { - "label": "", - "description": "", - "placeholder": "", - "loading": "", - "noWebsites": "", - "configureIntegrationFirst": "" + "label": "Site Web", + "description": "Sélectionnez un site web suivi par votre instance Umami", + "placeholder": "Sélectionnez un site", + "loading": "Chargement des sites Web...", + "noWebsites": "Aucun site web trouvé", + "configureIntegrationFirst": "Connectez d'abord une intégration Umami" }, "timeFrame": { - "label": "", - "description": "", + "label": "Intervalle de temps", + "description": "La période d'affichage des données du visiteur pour", "option": { - "today": "", - "24h": "", - "7d": "", - "30d": "", - "month": "", - "lastMonth": "" + "today": "Aujourd'hui", + "24h": "Dernières 24 heures", + "7d": "7 derniers jours", + "30d": "30 derniers jours", + "month": "Ce mois", + "lastMonth": "Le mois dernier" } }, "eventName": { - "label": "", - "description": "", - "placeholder": "", - "loading": "", - "noEvents": "", - "none": "", + "label": "Evénement", + "description": "Afficher le total des occurrences d'un événement spécifique", + "placeholder": "Sélectionner un événement", + "loading": "Chargement des événements...", + "noEvents": "Aucun événement trouvé", + "none": "Aucun événement", "configureWebsiteFirst": "" }, "chartType": { @@ -2577,15 +2602,15 @@ "direct": "" }, "topReferrers": { - "direct": "" + "direct": "Directe" }, "events": { - "selectPrompt": "" + "selectPrompt": "Sélectionnez les événements à afficher" }, "error": { "noIntegration": "", - "noWebsite": "", - "noData": "", + "noWebsite": "Aucun site web configuré", + "noData": "Aucune donnée disponible", "internalServerError": "" } }, @@ -2685,10 +2710,10 @@ } }, "paperlessNgx": { - "name": "", - "description": "", - "documentsTotal": "", - "documentsInbox": "", + "name": "Paperless-ngx", + "description": "Aperçu de votre archive de documents Paperless-ngx", + "documentsTotal": "Nombre total de documents", + "documentsInbox": "Boîte de réception", "correspondents": "", "tags": "", "documentTypes": "", @@ -2697,7 +2722,7 @@ "total": "", "option": { "showInboxRatio": { - "label": "", + "label": "Afficher le ratio de la boîte de réception", "description": "" }, "showInboxRing": { @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "Afficher les informations de la photo", "description": "Afficher la date et l'index de la photo en bas du diaporama" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "Diaporama de photos dans votre album Immich", @@ -3133,6 +3162,9 @@ "provider": { "label": "Fournisseur" }, + "providerUrl": { + "label": "URL du fournisseur" + }, "identifier": { "label": "Identifiant", "placeholder": "Nom ou Propriétaire / Nom" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "Aucun fournisseur", "tooltip": "Le fournisseur n'a pas pu être analysé, veuillez le définir manuellement après l'importation des images" + }, + "tokens": { + "label": "Tokens du fournisseur", + "token": "Token", + "configured": "Token configuré", + "notConfigured": "Aucun token défini", + "save": "Sauvegarder" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "Identifiant non valide", "noMatchingVersion": "Aucune version correspondante trouvée", "noReleasesFound": "Aucune publication trouvée", - "noProviderSeleceted": "Aucun fournisseur sélectionné", - "noProviderResponse": "Aucune réponse du fournisseur" + "noProviderSelected": "Aucun fournisseur sélectionné", + "noProviderResponse": "Aucune réponse du fournisseur", + "unexpected": "Erreur de fournisseur inattendue" } } }, @@ -3299,24 +3339,24 @@ "option": {} }, "speedtestTracker": { - "name": "", + "name": "Suivi de test de vitesse", "description": "", "option": { "showLatestResult": { - "label": "" + "label": "Afficher le dernier résultat" }, "showStats": { - "label": "" + "label": "Afficher les moyennes" }, "showRecentResults": { - "label": "" + "label": "Afficher le dernier résultat" }, "showPingGraph": { - "label": "" + "label": "Afficher le graphique de ping" } }, - "latestResult": "", - "averages": "", + "latestResult": "Dernier résultat", + "averages": "Moyennes", "tests": "", "recentResults": "", "recentResultsSpeed": "", @@ -3351,7 +3391,7 @@ }, "showTotalMonitors": { "label": "", - "description": "" + "description": "Afficher le nombre total de moniteurs configurés" }, "showUpCount": { "label": "", @@ -3366,35 +3406,35 @@ "description": "" } }, - "averageUptime": "", + "averageUptime": "Temps de disponibilité moyen (24h)", "stats": { - "totalMonitors": "", + "totalMonitors": "Nombre total de moniteurs", "upCount": "", "downCount": "", "pausedCount": "" }, "error": { - "internalServerError": "" + "internalServerError": "Impossible de récupérer les données de disponibilité" } }, "audioStats": { - "name": "", + "name": "Statistiques audio", "description": "", "option": { "showArtists": { - "label": "", - "description": "" + "label": "Afficher l'artiste", + "description": "Afficher le nombre total d'artistes dans votre bibliothèque musicale (Navidrome)" }, "showAlbums": { - "label": "", - "description": "" + "label": "Afficher l'album", + "description": "Afficher le nombre total d'albums dans votre bibliothèque musicale (Navidrome)" }, "showSongs": { - "label": "", + "label": "Afficher les titres", "description": "" }, "showNowPlaying": { - "label": "", + "label": "Afficher la lecture en cours", "description": "" }, "showNowPlayingList": { @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "Afficher la barre d'utilisation en arrière-plan" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "Non trouvé", "text": "Impossible de trouver la ressource demandée" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "Accueil", @@ -4499,86 +4555,14 @@ "disabled": "Désactivé" }, "job": { - "minecraftServerStatus": { - "label": "État du serveur Minecraft" - }, - "timetable": { - "label": "" - }, "iconsUpdater": { "label": "Mise à jour des icônes" }, "analytics": { "label": "Statistiques" }, - "smartHomeEntityState": { - "label": "État de l'entité Maison Connectée" - }, "ping": { "label": "Pings" - }, - "mediaServer": { - "label": "Serveur multimédia" - }, - "mediaOrganizer": { - "label": "Organisateurs de Médias" - }, - "downloads": { - "label": "Téléchargements" - }, - "mediaRequestStats": { - "label": "Statistiques des demandes de médias" - }, - "mediaRequestList": { - "label": "Liste des demandes de médias" - }, - "rssFeeds": { - "label": "Flux RSS" - }, - "indexerManager": { - "label": "Gestionnaire d’indexeur" - }, - "healthMonitoring": { - "label": "Moniteur de santé" - }, - "dnsHole": { - "label": "Données du puit DNS" - }, - "updateChecker": { - "label": "Vérificateur de mise à jour" - }, - "mediaTranscoding": { - "label": "Transcodage des médias" - }, - "networkController": { - "label": "Contrôleur réseau" - }, - "refreshNotifications": { - "label": "Mise à jour des notifications" - }, - "dockerContainers": { - "label": "Conteneurs Docker" - }, - "firewallCpu": { - "label": "Processeur du pare-feu" - }, - "firewallMemory": { - "label": "Mémoire du pare-feu" - }, - "firewallVersion": { - "label": "Version du pare-feu" - }, - "firewallInterfaces": { - "label": "Interfaces du pare-feu" - }, - "weather": { - "label": "Météo" - }, - "tracearr": { - "label": "Tracearr" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "Impossible de récupérer les conteneurs Docker" + "internalServerError": "Impossible de récupérer les conteneurs Docker", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/he.json b/packages/translation/src/lang/he.json index 5ecba1a8c..f2e575553 100644 --- a/packages/translation/src/lang/he.json +++ b/packages/translation/src/lang/he.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "", "speedtest": "", "analytics": "", + "vpn": "", "mediaLibrary": "", "uptimeMonitoring": "" }, @@ -1412,7 +1413,10 @@ "here": "כאן", "iconPicker": { "label": "קישור של הסמל", - "header": "הקלד שם או אובייקטים לסינון עבור אייקונים... Homarr יחפש עבורך סמלים של {countIcons} ." + "header": "הקלד שם או אובייקטים לסינון עבור אייקונים... Homarr יחפש עבורך סמלים של {countIcons} .", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "עבור למצב כהה", "switchToLightMode": "עבור למצב בהיר", "management": "ניהול", + "docker": "", "preferences": "העדפות שלך", "logout": "התנתק/י", "login": "התחבר/י", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "הגדרות" + "settings": "הגדרות", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "", "noStation": "", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "הצג מידע על מערכת הקבצים" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "כרטיסיית ברירת מחדל" }, @@ -2352,6 +2368,9 @@ "name": "סטטיסטיקות דוקר", "description": "סטטיסטיקות של המכולות שלך (ניתן להוסיף ווידג'ט זה רק עם הרשאות מנהל)", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "אפשר מיון פריטים" }, @@ -2405,6 +2424,12 @@ "title": "" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "", "description": "" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "", @@ -3133,6 +3162,9 @@ "provider": { "label": "ספק" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "מזהה", "placeholder": "שם או בעלים/שם" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "ללא ספק", "tooltip": "לא ניתן היה לנתח את הספק, אנא הגדר אותו ידנית לאחר ייבוא ​​התמונות" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "מזהה לא חוקי", "noMatchingVersion": "לא נמצאה גרסה תואמת", "noReleasesFound": "לא נמצאו שיחרורים", - "noProviderSeleceted": "לא נבחר ספק", - "noProviderResponse": "אין תשובה מהספק" + "noProviderSelected": "", + "noProviderResponse": "אין תשובה מהספק", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "לא נמצא", "text": "לא ניתן לאתר את המשאב המבוקש" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "מסך הבית", @@ -4499,86 +4555,14 @@ "disabled": "מושבת" }, "job": { - "minecraftServerStatus": { - "label": "סטטוס שרת מיינקראפט" - }, - "timetable": { - "label": "" - }, "iconsUpdater": { "label": "עדכון אייקונים" }, "analytics": { "label": "ניתוחים" }, - "smartHomeEntityState": { - "label": "סטטוס יישות בית חכם" - }, "ping": { "label": "פינגים" - }, - "mediaServer": { - "label": "שרת מדיה" - }, - "mediaOrganizer": { - "label": "מארגני מדיה" - }, - "downloads": { - "label": "הורדות" - }, - "mediaRequestStats": { - "label": "סטטוס בקשות מדיה" - }, - "mediaRequestList": { - "label": "רשימת בקשות מדיה" - }, - "rssFeeds": { - "label": "הזנות RSS" - }, - "indexerManager": { - "label": "מנהל אינדקסים" - }, - "healthMonitoring": { - "label": "ניטור בריאות המערכת" - }, - "dnsHole": { - "label": "נתוני חור DNS" - }, - "updateChecker": { - "label": "בודק עדכונים" - }, - "mediaTranscoding": { - "label": "המרת קידוד מדיה" - }, - "networkController": { - "label": "בקר רשת" - }, - "refreshNotifications": { - "label": "עדכון התראות" - }, - "dockerContainers": { - "label": "מכולות דוקר" - }, - "firewallCpu": { - "label": "מעבד חומת האש" - }, - "firewallMemory": { - "label": "זיכרון חומת אש" - }, - "firewallVersion": { - "label": "גרסת חומת אש" - }, - "firewallInterfaces": { - "label": "ממשקי חומת אש" - }, - "weather": { - "label": "מזג אוויר" - }, - "tracearr": { - "label": "" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "נכשל בקבלת קונטיינרי דוקר" + "internalServerError": "נכשל בקבלת קונטיינרי דוקר", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/hr.json b/packages/translation/src/lang/hr.json index 1f7d5013b..55ebb428b 100644 --- a/packages/translation/src/lang/hr.json +++ b/packages/translation/src/lang/hr.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "", "speedtest": "", "analytics": "", + "vpn": "", "mediaLibrary": "", "uptimeMonitoring": "" }, @@ -1412,7 +1413,10 @@ "here": "", "iconPicker": { "label": "", - "header": "" + "header": "", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "", "switchToLightMode": "", "management": "", + "docker": "", "preferences": "Vaše preferencije", "logout": "", "login": "Prijaviti se", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "Postavke" + "settings": "Postavke", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "", "noStation": "", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -2352,6 +2368,9 @@ "name": "", "description": "", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "" }, @@ -2405,6 +2424,12 @@ "title": "" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "", "description": "" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "", @@ -3133,6 +3162,9 @@ "provider": { "label": "" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "", "placeholder": "" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "", "tooltip": "" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "", "noMatchingVersion": "", "noReleasesFound": "", - "noProviderSeleceted": "", - "noProviderResponse": "" + "noProviderSelected": "", + "noProviderResponse": "", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "", "text": "" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "Dom", @@ -4499,86 +4555,14 @@ "disabled": "" }, "job": { - "minecraftServerStatus": { - "label": "" - }, - "timetable": { - "label": "" - }, "iconsUpdater": { "label": "" }, "analytics": { "label": "" }, - "smartHomeEntityState": { - "label": "" - }, "ping": { "label": "" - }, - "mediaServer": { - "label": "Medijski poslužitelj" - }, - "mediaOrganizer": { - "label": "" - }, - "downloads": { - "label": "" - }, - "mediaRequestStats": { - "label": "" - }, - "mediaRequestList": { - "label": "" - }, - "rssFeeds": { - "label": "" - }, - "indexerManager": { - "label": "" - }, - "healthMonitoring": { - "label": "" - }, - "dnsHole": { - "label": "" - }, - "updateChecker": { - "label": "" - }, - "mediaTranscoding": { - "label": "" - }, - "networkController": { - "label": "" - }, - "refreshNotifications": { - "label": "" - }, - "dockerContainers": { - "label": "" - }, - "firewallCpu": { - "label": "" - }, - "firewallMemory": { - "label": "" - }, - "firewallVersion": { - "label": "" - }, - "firewallInterfaces": { - "label": "" - }, - "weather": { - "label": "" - }, - "tracearr": { - "label": "" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "" + "internalServerError": "", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/hu.json b/packages/translation/src/lang/hu.json index 1c8d473d7..f9dbaf927 100644 --- a/packages/translation/src/lang/hu.json +++ b/packages/translation/src/lang/hu.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "", "speedtest": "", "analytics": "", + "vpn": "", "mediaLibrary": "", "uptimeMonitoring": "" }, @@ -1412,7 +1413,10 @@ "here": "itt", "iconPicker": { "label": "Ikonhivatkozás", - "header": "Írjon be nevet vagy objektumokat az ikonok szűréséhez... A Homarr a {countIcons} ikonok között keres." + "header": "Írjon be nevet vagy objektumokat az ikonok szűréséhez... A Homarr a {countIcons} ikonok között keres.", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "Váltás sötét módba", "switchToLightMode": "Váltás világos módra", "management": "Kezelés", + "docker": "", "preferences": "Saját preferenciák", "logout": "Kijelentkezés", "login": "Bejelentkezés", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "Beállítások" + "settings": "Beállítások", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "Indulások {station}", "noStation": "Nincs kiválasztott állomás", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "Állomás" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "Fájlrendszer-információk megjelenítése" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Alapértelmezett fül" }, @@ -2352,6 +2368,9 @@ "name": "Docker statisztika", "description": "A konténereid statisztikái (Ezt a widgetet csak rendszergazdai jogosultsággal lehet hozzáadni)", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "Az elemek rendezésének engedélyezése" }, @@ -2405,6 +2424,12 @@ "title": "Példányok" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "Fotóadatok megjelenítése", "description": "Kép dátumának és az aktuális indexszámnak a megjelenítése a diavetítés alján" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "Képek diavetítése az Immich-albumodból", @@ -3133,6 +3162,9 @@ "provider": { "label": "Szolgáltató" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "Azonosító", "placeholder": "Név vagy tulajdonos/név" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "Nincs Szolgáltató", "tooltip": "A szolgáltató adatai nem értelmezhetők; kérjük, az importálás után állítsa be azokat manuálisan" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "", "noMatchingVersion": "", "noReleasesFound": "", - "noProviderSeleceted": "", - "noProviderResponse": "" + "noProviderSelected": "", + "noProviderResponse": "", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "Háttérhasználati sáv megjelenítése" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "", "text": "" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "Nyitólap", @@ -4499,86 +4555,14 @@ "disabled": "" }, "job": { - "minecraftServerStatus": { - "label": "" - }, - "timetable": { - "label": "" - }, "iconsUpdater": { "label": "" }, "analytics": { "label": "" }, - "smartHomeEntityState": { - "label": "" - }, "ping": { "label": "" - }, - "mediaServer": { - "label": "Médiakiszolgáló" - }, - "mediaOrganizer": { - "label": "" - }, - "downloads": { - "label": "Letöltések" - }, - "mediaRequestStats": { - "label": "" - }, - "mediaRequestList": { - "label": "" - }, - "rssFeeds": { - "label": "" - }, - "indexerManager": { - "label": "" - }, - "healthMonitoring": { - "label": "" - }, - "dnsHole": { - "label": "" - }, - "updateChecker": { - "label": "" - }, - "mediaTranscoding": { - "label": "" - }, - "networkController": { - "label": "" - }, - "refreshNotifications": { - "label": "" - }, - "dockerContainers": { - "label": "" - }, - "firewallCpu": { - "label": "Tűzfal CPU" - }, - "firewallMemory": { - "label": "Tűzfal Memória" - }, - "firewallVersion": { - "label": "" - }, - "firewallInterfaces": { - "label": "" - }, - "weather": { - "label": "" - }, - "tracearr": { - "label": "" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "" + "internalServerError": "", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/it.json b/packages/translation/src/lang/it.json index e170a2565..63629d558 100644 --- a/packages/translation/src/lang/it.json +++ b/packages/translation/src/lang/it.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "Monitoraggio", "speedtest": "Speed test", "analytics": "Analytics", + "vpn": "VPN", "mediaLibrary": "Media Library", "uptimeMonitoring": "Uptime Monitoring" }, @@ -1412,7 +1413,10 @@ "here": "qui", "iconPicker": { "label": "Icona URL", - "header": "Digita il nome o gli oggetti da filtrare per le icone... Homarr cercherà tra le icone {countIcons} per te." + "header": "Digita il nome o gli oggetti da filtrare per le icone... Homarr cercherà tra le icone {countIcons} per te.", + "headerLoading": "Digita per cercare icone…", + "noResults": "Nessuna icona corrisponde a \"{search}\".", + "error": "Impossibile caricare le icone." }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "Passa alla modalità scura", "switchToLightMode": "Passa al tema chiaro", "management": "Gestione", + "docker": "Docker", "preferences": "Le tue impostazioni", "logout": "Disconnettiti", "login": "Accedi", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "Impostazioni" + "settings": "Impostazioni", + "refresh": "Aggiorna", + "options": "Opzioni", + "actions": "Azioni" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "Partenze {station}", "noStation": "Nessuna stazione selezionata", "option": { + "baseUrl": { + "label": "Url Search.ch" + }, "station": { "label": "Stazione" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "Mostra Informazioni Filesystem" }, + "visibleStorageVolumes": { + "label": "Volumi di archiviazione visibili", + "description": "Disponibile solo se tutte le integrazioni selezionate sono Synology. Lasciare vuoto per visualizzare tutti i volumi di archiviazione rilevati.", + "placeholder": "Selezionare volumi di archiviazione" + }, "defaultTab": { "label": "Scheda predefinita" }, @@ -2352,6 +2368,9 @@ "name": "Statistiche del docker", "description": "Statistiche dei tuoi contenitori (questo widget può essere aggiunto solo con privilegi di amministratore)", "option": { + "columns": { + "label": "Colonne da mostrare" + }, "enableRowSorting": { "label": "Abilita l'ordinamento degli elementi" }, @@ -2405,6 +2424,12 @@ "title": "Istanze" } }, + "vpn": { + "name": "VPN", + "description": "Panoramica delle tue connessioni VPN", + "serviceUnavailable": "VPN non disponibile", + "option": {} + }, "ups": { "name": "UPS", "description": "Monitora lo stato dei tuoi dispositivi UPS tramite un server NUT", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "Mostra informazioni foto", "description": "Visualizza la data dell'immagine e l'indice corrente nella parte inferiore della presentazione" + }, + "randomizePhotos": { + "label": "Immagini casuali", + "description": "Mostra le foto in ordine casuale invece di iniziare sempre con lo stesso ordine degli album." } }, "description": "Presentazione delle immagini nel tuo album Immich", @@ -3133,6 +3162,9 @@ "provider": { "label": "Provider" }, + "providerUrl": { + "label": "URL Provider" + }, "identifier": { "label": "Identificatore", "placeholder": "Nome o Proprietario/Nome" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "Nessun Provider", "tooltip": "Impossibile analizzare il provider, impostarlo manualmente dopo aver importato le immagini" + }, + "tokens": { + "label": "Token Provider", + "token": "Token", + "configured": "Token configurato", + "notConfigured": "Nessun token impostato", + "save": "Salva" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "Identificatore non valido", "noMatchingVersion": "Nessuna versione corrispondente trovata", "noReleasesFound": "Nessun rilascio trovato", - "noProviderSeleceted": "Nessun provider selezionato", - "noProviderResponse": "Nessuna risposta dal provider" + "noProviderSelected": "Nessun provider selezionato", + "noProviderResponse": "Nessuna risposta dal provider", + "unexpected": "Errore provider inaspettato" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "Mostra barra di utilizzo in background" + }, + "visibleStorageVolumes": { + "label": "Volumi di archiviazione visibili", + "description": "Disponibile solo se tutte le integrazioni selezionate sono Synology. Lasciare vuoto per visualizzare tutti i volumi di archiviazione rilevati.", + "placeholder": "Selezionare volumi di archiviazione" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "Impossibile connettersi a Beszel. Controlla le tue impostazioni di integrazione." }, + "empty": { + "noSystems": "Nessun sistema trovato" + }, "metric": { "cpu": "CPU", "memory": "Memoria", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "Seleziona sistema", + "empty": { + "noSystems": "Nessun sistema trovato" + }, "error": { "internalServerError": "Impossibile connettersi a Beszel. Controlla le tue impostazioni di integrazione.", "liveConnectionFailed": "Connessione Live non riuscita. L'istanza di Beszel potrebbe non supportare lo streaming in tempo reale.", @@ -4133,6 +4184,11 @@ "title": "Non trovato", "text": "Impossibile trovare la risorsa richiesta" }, + "error": { + "title": "Questa pagina non può essere caricata", + "text": "Si è verificato un errore inatteso durante il caricamento di questa pagina. La navigazione è ancora disponibile: prova a ricaricare o usa un'altra sezione dalla barra laterale.", + "digest": "Riferimento errore: {digest}" + }, "navbar": { "items": { "home": "Home", @@ -4499,86 +4555,14 @@ "disabled": "Disabilitato" }, "job": { - "minecraftServerStatus": { - "label": "Stato del server Minecraft" - }, - "timetable": { - "label": "Orario" - }, "iconsUpdater": { "label": "Aggiornatore Icone" }, "analytics": { "label": "Analytics" }, - "smartHomeEntityState": { - "label": "Stato dell'entità Smart Home" - }, "ping": { "label": "Ping" - }, - "mediaServer": { - "label": "Media Server" - }, - "mediaOrganizer": { - "label": "Organizzatori Media" - }, - "downloads": { - "label": "Scaricamenti" - }, - "mediaRequestStats": { - "label": "Statistiche richieste media" - }, - "mediaRequestList": { - "label": "Elenco richieste media" - }, - "rssFeeds": { - "label": "Feeds RSS" - }, - "indexerManager": { - "label": "Gestione Indicizzatori" - }, - "healthMonitoring": { - "label": "Monitoraggio integrità del sistema" - }, - "dnsHole": { - "label": "Dati di filtraggio DNS" - }, - "updateChecker": { - "label": "Controllo aggiornamenti" - }, - "mediaTranscoding": { - "label": "Transcodifica del media" - }, - "networkController": { - "label": "Controller di rete" - }, - "refreshNotifications": { - "label": "Aggiornamento Notifiche" - }, - "dockerContainers": { - "label": "Containers docker" - }, - "firewallCpu": { - "label": "CPU Firewall" - }, - "firewallMemory": { - "label": "Memoria Firewall" - }, - "firewallVersion": { - "label": "Versione Firewall" - }, - "firewallInterfaces": { - "label": "Interfacce Firewall" - }, - "weather": { - "label": "Meteo" - }, - "tracearr": { - "label": "Tracearr" - }, - "umamiActiveVisitors": { - "label": "Visitatori Attivi Umami" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "Recupero dei container Docker non riuscito" + "internalServerError": "Recupero dei container Docker non riuscito", + "noColumns": "Seleziona almeno una colonna nelle impostazioni dei widget" } }, "kubernetes": { diff --git a/packages/translation/src/lang/ja.json b/packages/translation/src/lang/ja.json index 1be05e8c8..e6018ccbd 100644 --- a/packages/translation/src/lang/ja.json +++ b/packages/translation/src/lang/ja.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "", "speedtest": "", "analytics": "", + "vpn": "", "mediaLibrary": "", "uptimeMonitoring": "" }, @@ -1412,7 +1413,10 @@ "here": "こちら", "iconPicker": { "label": "アイコン URL", - "header": "アイコンをフィルタするために、名前またはオブジェクトを入力してください... Homarrは {countIcons} アイコンを検索します。" + "header": "アイコンをフィルタするために、名前またはオブジェクトを入力してください... Homarrは {countIcons} アイコンを検索します。", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "ダークモードに切り替え", "switchToLightMode": "ライトモードに切り替え", "management": "管理", + "docker": "", "preferences": "個人設定", "logout": "ログアウト", "login": "ログイン", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "設定" + "settings": "設定", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "{station} 出発時刻", "noStation": "駅が選択されていません", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "駅" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "ファイルシステム情報を表示" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "デフォルトのタブ" }, @@ -2352,6 +2368,9 @@ "name": "Dockerのステータス", "description": "あなたのコンテナの統計(このウィジェットは管理者権限でのみ追加できます)", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "アイテムの並び替えを有効にする" }, @@ -2405,6 +2424,12 @@ "title": "インスタンス" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "写真情報を表示", "description": "スライドショーの下部に画像の日付と現在の索引を表示します" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "Immichアルバムの写真のスライド", @@ -3133,6 +3162,9 @@ "provider": { "label": "プロバイダー" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "識別名", "placeholder": "名前、または所有者名" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "プロバイダーがありません", "tooltip": "プロバイダーを解析できませんでした。画像をインポートしたあとで、手動で設定してください" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "無効な識別子", "noMatchingVersion": "一致するバージョンが見つかりません", "noReleasesFound": "リリースが見つかりませんでした", - "noProviderSeleceted": "プロバイダーが選択されていません", - "noProviderResponse": "プロバイダーからの応答がありません" + "noProviderSelected": "", + "noProviderResponse": "プロバイダーからの応答がありません", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "バックグラウンドの使用状況バーを表示" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "見つかりません", "text": "要求されたリソースが見つかりませんでした" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "ホーム", @@ -4499,86 +4555,14 @@ "disabled": "無効" }, "job": { - "minecraftServerStatus": { - "label": "Minecraft サーバーの状態" - }, - "timetable": { - "label": "タイムテーブル" - }, "iconsUpdater": { "label": "アイコンアップデータ" }, "analytics": { "label": "統計情報" }, - "smartHomeEntityState": { - "label": "スマートホームエンティティの状態" - }, "ping": { "label": "Pings" - }, - "mediaServer": { - "label": "メディアサーバー" - }, - "mediaOrganizer": { - "label": "メディアオーガナイザー" - }, - "downloads": { - "label": "ダウンロード" - }, - "mediaRequestStats": { - "label": "メディアリクエストの統計情報" - }, - "mediaRequestList": { - "label": "メディアリクエストのリスト" - }, - "rssFeeds": { - "label": "RSS フィード" - }, - "indexerManager": { - "label": "インデクサーマネージャー" - }, - "healthMonitoring": { - "label": "ヘルスモニタ" - }, - "dnsHole": { - "label": "DNS Holeデータ" - }, - "updateChecker": { - "label": "アップデートチェッカー" - }, - "mediaTranscoding": { - "label": "メディアのトランスコーディング" - }, - "networkController": { - "label": "ネットワークコントローラ" - }, - "refreshNotifications": { - "label": "通知アップデーター" - }, - "dockerContainers": { - "label": "Docker コンテナ" - }, - "firewallCpu": { - "label": "ファイアウォールの CPU" - }, - "firewallMemory": { - "label": "ファイアウォールのメモリ" - }, - "firewallVersion": { - "label": "ファイアウォールのバージョン" - }, - "firewallInterfaces": { - "label": "ファイアウォールのインターフェース" - }, - "weather": { - "label": "天気" - }, - "tracearr": { - "label": "Tracearr" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "Docker コンテナの取得に失敗しました" + "internalServerError": "Docker コンテナの取得に失敗しました", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/ko.json b/packages/translation/src/lang/ko.json index 8ddf946f3..e665078ee 100644 --- a/packages/translation/src/lang/ko.json +++ b/packages/translation/src/lang/ko.json @@ -288,10 +288,10 @@ "changePingIconsEnabled": { "notification": { "success": { - "message": "" + "message": "Ping 아이콘 전환 완료" }, "error": { - "message": "" + "message": "Ping 아이콘을 전환할 수 없습니다" } } }, @@ -701,56 +701,56 @@ }, "action": { "create": "생성", - "import": "", - "save": "", - "delete": "", - "deleteConfirm": "", - "export": "", - "addMapping": "", - "addColumn": "", - "edit": "", - "removeMapping": "", - "removeColumn": "", - "addItem": "", - "removeItem": "", - "addBar": "", - "duplicate": "", - "copyAiPrompt": "", - "enable": "", - "disable": "", - "menu": "" + "import": "가져오기", + "save": "저장", + "delete": "사용자 지정 위젯 삭제", + "deleteConfirm": "\"{name}\"을(를) 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", + "export": "내보내기", + "addMapping": "매핑 추가", + "addColumn": "열 추가", + "edit": "편집", + "removeMapping": "매핑 제거", + "removeColumn": "열 제거", + "addItem": "항목 추가", + "removeItem": "항목 제거", + "addBar": "바 추가", + "duplicate": "복사", + "copyAiPrompt": "AI 프롬프트 복사", + "enable": "활성화", + "disable": "비활성화", + "menu": "동작" }, "field": { - "name": "", - "description": "", - "iconUrl": "", - "url": "", - "authType": "", - "headerName": "", - "method": "", - "requestBody": "", - "displayType": "", - "jsonPath": "", - "jsonPathHint": "", - "label": "", - "unit": "", - "mappings": "", - "tablePath": "", - "tablePathHint": "", - "columns": "", - "header": "", - "items": "", - "gridColumns": "", - "cardStyle": "", - "bars": "", - "valuePath": "", - "maxPath": "", - "showPercentage": "", - "barSize": "", - "goodValues": "", - "dotSize": "", - "valueSize": "", - "labelPosition": "", + "name": "이름", + "description": "설명", + "iconUrl": "아이콘 URL", + "url": "URL", + "authType": "인증 유형", + "headerName": "헤더/쿼리 파라미터 이름", + "method": "메서드", + "requestBody": "요청 본문", + "displayType": "표시 유형", + "jsonPath": "JSON 경로", + "jsonPathHint": "JSONPath 문법으로 값을 추출하십시오. 예: $.data.temperature 또는 $.items[0].name", + "label": "레이블", + "unit": "단위", + "mappings": "매핑", + "tablePath": "JSON 경로 배열", + "tablePathHint": "응답에서 배열이 위치한 경로입니다. 예: $.data.items", + "columns": "열", + "header": "헤더", + "items": "항목", + "gridColumns": "그리드 열 개수", + "cardStyle": "카드 스타일", + "bars": "바", + "valuePath": "JSON 경로 값", + "maxPath": "JSON 경로 최댓값", + "showPercentage": "백분율 표시", + "barSize": "바 크기", + "goodValues": "정상 값(쉼표로 구분)", + "dotSize": "점 크기", + "valueSize": "값 크기", + "labelPosition": "레이블 위치", "kvLayout": "레이아웃", "striped": "", "compact": "컴팩트 모드", @@ -782,22 +782,22 @@ "valuePath": "$.data.used", "maxPath": "$.data.total", "goodValues": "", - "color": "" + "color": "파랑" }, "cardStyleOption": { - "filled": "", + "filled": "채워짐", "outline": "", "subtle": "" }, "layoutOption": { - "list": "", - "grid": "" + "list": "리스트", + "grid": "그리드" }, "sizeOption": { - "sm": "", - "md": "", - "lg": "", - "xl": "" + "sm": "작음", + "md": "중간", + "lg": "큼", + "xl": "매우 큼" }, "labelPositionOption": { "above": "", @@ -805,8 +805,8 @@ }, "fieldset": { "general": "", - "connection": "", - "display": "" + "connection": "API 연결", + "display": "표시 설정" }, "displayTypeDescription": { "singleValue": "", @@ -821,42 +821,42 @@ "customJsx": "" }, "method": { - "GET": "", - "POST": "", - "PUT": "", - "DELETE": "", - "PATCH": "" + "GET": "GET", + "POST": "POST", + "PUT": "PUT", + "DELETE": "DELETE", + "PATCH": "PATCH" }, "authType": { - "none": "", - "bearer": "", - "basic": "", - "apiKeyHeader": "", - "apiKeyQuery": "" + "none": "없음", + "bearer": "Bearer 토큰", + "basic": "기본 인증", + "apiKeyHeader": "API 키(헤더)", + "apiKeyQuery": "API 키(쿼리)" }, "displayType": { - "singleValue": "", - "keyValue": "", - "table": "", - "statGrid": "", - "progressBars": "", - "statusIndicator": "", - "countGrid": "", - "raw": "", - "actionButton": "", - "customJsx": "" + "singleValue": "단일 값", + "keyValue": "키-값", + "table": "테이블", + "statGrid": "스택 그리드", + "progressBars": "진행 바", + "statusIndicator": "상태 인디케이터", + "countGrid": "카운트 그리드", + "raw": "원시 JSON", + "actionButton": "동작 버튼", + "customJsx": "사용자 지정 JSX" }, "secret": { - "apiKey": "", - "username": "", - "password": "", - "savedPlaceholder": "" + "apiKey": "API 키/토큰", + "username": "사용자명", + "password": "비밀번호", + "savedPlaceholder": "••••••• (저장됨, 유지하려면 공백)" }, "preview": { - "title": "", - "test": "", - "hint": "", - "rawResponse": "" + "title": "미리보기", + "test": "테스트", + "hint": "현재 설정으로 API 응답을 미리 보려면 테스트를 클릭하십시오.", + "rawResponse": "원본 JSON 응답" }, "notification": { "created": "\"{name}\" 위젯 생성 완료.", @@ -892,14 +892,14 @@ "integration": { "page": { "list": { - "title": "", + "title": "통합", "search": "", "noResults": { "title": "" } }, "create": { - "title": "", + "title": "새 {name} 통합", "notification": { "success": { "title": "", @@ -917,9 +917,9 @@ "label": "" }, "new": { - "title": "", + "title": "신규", "url": { - "label": "", + "label": "앱 URL", "description": "" } } @@ -927,7 +927,7 @@ } }, "edit": { - "title": "", + "title": "{name} 통합 편집", "notification": { "success": { "title": "", @@ -940,8 +940,8 @@ }, "app": { "action": { - "add": "", - "remove": "", + "add": "앱 연결", + "remove": "연결 해제", "select": "" } } @@ -951,11 +951,11 @@ "message": "", "notification": { "success": { - "title": "", + "title": "삭제 성공", "message": "" }, "error": { - "title": "", + "title": "삭제 실패", "message": "" } } @@ -987,7 +987,7 @@ } }, "action": { - "create": "" + "create": "새 통합" }, "grid": { "widgetCount": "", @@ -1004,9 +1004,9 @@ "downloadClient": "", "usenet": "", "torrent": "", - "miscellaneous": "", - "smartHomeServer": "", - "indexerManager": "", + "miscellaneous": "기타", + "smartHomeServer": "스마트 홈", + "indexerManager": "인덱서", "healthMonitoring": "건강", "search": "검색", "mediaTranscoding": "트랜스코딩", @@ -1021,13 +1021,14 @@ "mediaMonitoring": "모니터링", "speedtest": "속도 테스트", "analytics": "분석", + "vpn": "VPN", "mediaLibrary": "미디어 라이브러리", "uptimeMonitoring": "업타임 모니터링" }, "testConnection": { "action": { - "create": "", - "edit": "" + "create": "연결 테스트 및 생성", + "edit": "연결 테스트 및 저장" }, "error": { "common": { @@ -1036,45 +1037,45 @@ } }, "unknown": { - "title": "", + "title": "알 수 없는 오류", "description": "" }, "parse": { - "title": "", + "title": "파싱 오류", "description": "" }, "authorization": { - "title": "", + "title": "인증 오류", "description": "" }, "statusCode": { - "title": "", + "title": "응답 오류", "description": "", "otherDescription": "", "reason": { - "badRequest": "", - "notFound": "", - "tooManyRequests": "", - "internalServerError": "", - "serviceUnavailable": "", - "gatewayTimeout": "" + "badRequest": "잘못된 요청", + "notFound": "찾을 수 없음", + "tooManyRequests": "요청이 너무 많음", + "internalServerError": "내부 서버 오류", + "serviceUnavailable": "서비스 사용 불가", + "gatewayTimeout": "게이트웨이 시간 초과" } }, "certificate": { - "title": "", + "title": "인증서 오류", "description": { - "expired": "", + "expired": "인증서가 만료되었습니다.", "notYetValid": "", "untrusted": "", "hostnameMismatch": "" }, "alert": { "permission": { - "title": "", + "title": "권한 부족", "message": "" }, "hostnameMismatch": { - "title": "", + "title": "호스트 이름이 불일치함", "message": "" }, "extract": { @@ -1100,7 +1101,7 @@ }, "notification": { "success": { - "title": "", + "title": "신뢰하는 인증서", "message": "" }, "error": { @@ -1126,16 +1127,16 @@ } }, "details": { - "title": "", + "title": "세부 사항", "description": "", "content": { - "action": "", + "action": "항목 보기", "title": "" } } }, "request": { - "title": "", + "title": "요청 오류", "description": { "connection": { "hostUnreachable": "", @@ -1174,7 +1175,7 @@ "message": "" }, "commonError": { - "title": "", + "title": "연결 실패", "message": "" }, "badRequest": { @@ -1228,7 +1229,7 @@ } }, "secrets": { - "title": "", + "title": "비밀 구문", "lastUpdated": "", "notSet": { "label": "", @@ -1246,19 +1247,19 @@ "kind": { "username": { "label": "사용자 이름", - "newLabel": "" + "newLabel": "새 사용자 이름" }, "apiKey": { "label": "", - "newLabel": "" + "newLabel": "새 API 키" }, "password": { "label": "비밀번호", - "newLabel": "" + "newLabel": "새 패스워드" }, "tokenId": { "label": "", - "newLabel": "" + "newLabel": "새 토큰 ID" }, "realm": { "label": "", @@ -1266,35 +1267,35 @@ }, "personalAccessToken": { "label": "", - "newLabel": "" + "newLabel": "새 개인 액세스 토큰(PAT)" }, "topic": { "label": "", - "newLabel": "" + "newLabel": "새 주제" }, "url": { "label": "", - "newLabel": "" + "newLabel": "새 URL" }, "opnsenseApiKey": { "label": "", - "newLabel": "" + "newLabel": "새 API 키(Key)" }, "opnsenseApiSecret": { "label": "", - "newLabel": "" + "newLabel": "새 API 키(Secret)" }, "githubAppId": { "label": "", - "newLabel": "" + "newLabel": "새 앱 ID" }, "githubInstallationId": { "label": "", - "newLabel": "" + "newLabel": "새 설치 ID" }, "privateKey": { "label": "", - "newLabel": "" + "newLabel": "새 개인 키" }, "slug": { "label": "", @@ -1412,7 +1413,10 @@ "here": "여기", "iconPicker": { "label": "아이콘 URL", - "header": "이름 또는 주제를 입력해 아이콘을 필터링하세요... Homarr가 {countIcons}에서 아이콘을 검색합니다." + "header": "이름 또는 주제를 입력해 아이콘을 필터링하세요... Homarr가 {countIcons}에서 아이콘을 검색합니다.", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "다크 모드로 전환", "switchToLightMode": "라이트 모드로 전환", "management": "관리", + "docker": "", "preferences": "기본 설정", "logout": "로그아웃", "login": "로그인", @@ -1522,7 +1527,7 @@ "section": { "dynamic": { "action": { - "create": "", + "create": "새 동적 섹션", "remove": "" }, "option": { @@ -1548,17 +1553,17 @@ } }, "action": { - "create": "", + "create": "새 카테고리", "edit": "", "remove": "", "moveUp": "위로 이동", "moveDown": "아래로 이동", "createAbove": "", "createBelow": "", - "openAllInNewTabs": "" + "openAllInNewTabs": "탭에서 모두 열기" }, "create": { - "title": "", + "title": "새 카테고리", "submit": "" }, "remove": { @@ -1571,29 +1576,32 @@ }, "menu": { "label": { - "create": "", + "create": "새 카테고리", "changePosition": "위치 변경", "settings": "" } }, "openAllInNewTabs": { - "title": "", - "text": "" + "title": "탭에서 모두 열기", + "text": "일부 브라우저는 보안상의 이유로 탭의 대량 열기를 차단할 수 있습니다. 브라우저가 이 작업을 차단했기 때문에 Homarr가 모든 창을 열지 못했습니다. \"팝업 창 열기\"를 허용한 후 다시 시도해 주십시오." } } }, "item": { "action": { - "create": "", - "import": "", - "edit": "", - "moveResize": "", - "duplicate": "", - "remove": "" + "create": "새 항목", + "import": "항목 가져오기", + "edit": "항목 편집", + "moveResize": "항목 이동 / 크기변경", + "duplicate": "항목 복사", + "remove": "항목 제거" }, "menu": { "label": { - "settings": "설정" + "settings": "설정", + "refresh": "새로고침", + "options": "옵션", + "actions": "동작" } }, "create": { @@ -1602,7 +1610,7 @@ "addToBoard": "" }, "moveResize": { - "title": "", + "title": "항목 이동 / 크기변경", "field": { "width": { "label": "너비" @@ -1611,21 +1619,21 @@ "label": "높이" }, "xOffset": { - "label": "" + "label": "X 오프셋" }, "yOffset": { - "label": "" + "label": "Y 오프셋" } } }, "edit": { - "title": "", + "title": "항목 편집", "tab": { - "widget": "", - "app": "" + "widget": "위젯 편집", + "app": "앱 편집" }, "app": { - "propagationNotice": "" + "propagationNotice": "이 앱의 변경 사항은 전체 대시보드에 적용됩니다." }, "advancedOptions": { "label": "고급 옵션", @@ -1829,44 +1837,44 @@ } }, "clock": { - "name": "", + "name": "날짜 및 시간", "description": "현재 날짜와 시간을 표시합니다.", "option": { "customTitleToggle": { - "label": "", - "description": "" + "label": "커스텀 제목/도시 표시", + "description": "시계 위에 커스텀 제목 또는 도시/국가 이름 표시." }, "customTitle": { - "label": "" + "label": "제목" }, "is24HourFormat": { - "label": "", - "description": "" + "label": "24시간 형식", + "description": "12시간 형식 대신 24시간 형식 사용" }, "showSeconds": { - "label": "" + "label": "초 단위 표시" }, "useCustomTimezone": { - "label": "" + "label": "고정 시간대 사용" }, "timezone": { - "label": "", - "description": "" + "label": "시간대", + "description": "IANA 표준 시간대 선택" }, "showDate": { - "label": "" + "label": "날짜 표시" }, "dateFormat": { - "label": "", - "description": "" + "label": "날짜 형식", + "description": "날짜를 표시하는 형식" }, "customTimeFormat": { - "label": "", - "description": "" + "label": "커스텀 시간 형식", + "description": "ISO 8601을 사용하여 시간 형식 지정 (다른 옵션 무시)" }, "customDateFormat": { - "label": "", - "description": "" + "label": "커스텀 날짜 형식", + "description": "ISO 8601을 사용하여 날짜 형식 지정 (다른 옵션 무시)" } } }, @@ -1895,6 +1903,9 @@ "title": "", "noStation": "", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "" } @@ -1902,71 +1913,71 @@ }, "notebook": { "name": "노트북", - "description": "", + "description": "마크다운을 지원하는 간단한 노트북 위젯", "option": { "showToolbar": { "label": "마크다운 작성에 도움이 되는 도구 모음 표시" }, "allowReadOnlyCheck": { - "label": "" + "label": "읽기 전용 모드에서 체크인 허용" }, "content": { "label": "노트북의 콘텐츠" } }, "controls": { - "bold": "", - "italic": "", - "strikethrough": "", - "underline": "", - "colorText": "", - "colorHighlight": "", - "code": "", - "clear": "", - "heading": "", - "align": "", - "blockquote": "", - "horizontalLine": "", - "bulletList": "", - "orderedList": "", - "checkList": "", - "increaseIndent": "", - "decreaseIndent": "", - "link": "", - "unlink": "", - "image": "", - "addTable": "", - "deleteTable": "", - "colorCell": "", - "mergeCell": "", - "addColumnLeft": "", - "addColumnRight": "", - "deleteColumn": "", - "addRowTop": "", - "addRowBelow": "", - "deleteRow": "" + "bold": "진하게", + "italic": "이탤릭", + "strikethrough": "취소선", + "underline": "밑줄", + "colorText": "글자 색상", + "colorHighlight": "하이라이트 색상", + "code": "코드", + "clear": "서식 초기화", + "heading": "제목 레벨 {level}", + "align": "텍스트 정렬: {position}", + "blockquote": "인용문", + "horizontalLine": "가로선", + "bulletList": "글머리 기호 목록", + "orderedList": "번호순 목록", + "checkList": "체크리스트", + "increaseIndent": "들여쓰기 늘리기", + "decreaseIndent": "들여쓰기 줄이기", + "link": "링크", + "unlink": "링크 제거", + "image": "이미지 삽입", + "addTable": "표 추가", + "deleteTable": "표 삭제", + "colorCell": "셀 색상", + "mergeCell": "셀 병합 토글", + "addColumnLeft": "앞에 열 추가", + "addColumnRight": "뒤에 열 추가", + "deleteColumn": "열 삭제", + "addRowTop": "앞 행 추가", + "addRowBelow": "뒤 행 추가", + "deleteRow": "행 삭제" }, "align": { "left": "왼쪽", - "center": "", + "center": "가운데", "right": "오른쪽" }, "popover": { - "clearColor": "", - "source": "", - "widthPlaceholder": "", - "columns": "", - "rows": "", + "clearColor": "색상 제거", + "source": "소스", + "widthPlaceholder": "% 또는 픽셀 값", + "columns": "열", + "rows": "행", "width": "너비", "height": "높이" }, - "placeholder": "", + "placeholder": "노트 작성 시작", "dismiss": { - "title": "", - "message": "", + "title": "변경사항 삭제", + "message": "노트북에 저장되지 않은 변경 사항이 있습니다. 삭제하시겠습니까?", "action": { - "discard": "", - "keepEditing": "" + "discard": "저장하지 않음", + "keepEditing": "계속 편집" } } }, @@ -1999,7 +2010,7 @@ "saveFailed": "" }, "iframe": { - "name": "", + "name": "iFrame", "description": "인터넷에서 콘텐츠를 퍼옵니다. 일부 웹사이트는 액세스를 제한할 수 있습니다.", "option": { "embedUrl": { @@ -2030,12 +2041,12 @@ "label": "지리적 위치 허용" }, "allowModals": { - "label": "" + "label": "모달 허용" } }, "error": { - "noUrl": "", - "unsupportedProtocol": "", + "noUrl": "iFrame URL 없음", + "unsupportedProtocol": "URL이 지원되지 않는 프로토콜을 사용합니다. ({supportedProtocols}) 중 하나를 사용하십시오", "noBrowerSupport": "브라우저가 iframe을 지원하지 않습니다. 브라우저를 업데이트하세요." } }, @@ -2150,27 +2161,27 @@ }, "calendar": { "name": "캘린더", - "description": "", + "description": "일정 기간 내에 캘린더 보기에 통합된 이벤트를 표시합니다", "duration": { - "allDay": "" + "allDay": "하루 종일" }, "option": { "releaseType": { - "label": "레이더 릴리스 유형", + "label": "Radarr 릴리즈 유형", "options": { - "inCinemas": "", - "digitalRelease": "", - "physicalRelease": "" + "inCinemas": "극장 상영", + "digitalRelease": "디지털 출시", + "physicalRelease": "실물 출시" } }, "filterPastMonths": { - "label": "" + "label": "과거 이벤트 표시 개월수" }, "filterFutureMonths": { - "label": "" + "label": "미래 이벤트 표시 개월수" }, "showUnmonitored": { - "label": "" + "label": "모니터링되지 않음 표시" } } }, @@ -2179,42 +2190,42 @@ "description": "설정한 위치의 현재 날씨 정보를 표시합니다.", "option": { "isFormatFahrenheit": { - "label": "" + "label": "화씨 온도" }, "disableTemperatureDecimals": { - "label": "" + "label": "온도 소수점 비활성화" }, "showCurrentWindSpeed": { - "label": "", - "description": "" + "label": "현재 풍속 표시", + "description": "현재 날씨 기준으로만 표시됩니다" }, "useImperialSpeed": { - "label": "" + "label": "mph 풍속 사용" }, "location": { "label": "날씨 위치" }, "showCity": { - "label": "" + "label": "도시 표시" }, "hasForecast": { - "label": "" + "label": "예보 표시" }, "forecastDayCount": { - "label": "", - "description": "" + "label": "예보 일수", + "description": "위젯이 충분히 넓지 않으면 표시되는 일수가 줄어듭니다" }, "dateFormat": { - "label": "", - "description": "" + "label": "날짜 형식", + "description": "날짜를 표시하는 형식" } }, - "currentWindSpeed": "", + "currentWindSpeed": "{currentWindSpeed}{unit}", "dailyForecast": { - "sunrise": "", - "sunset": "", - "maxWindSpeed": "", - "maxWindGusts": "" + "sunrise": "일출", + "sunset": "일몰", + "maxWindSpeed": "최대 풍속: {maxWindSpeed}{unit}", + "maxWindGusts": "최대 순간 풍속: {maxWindGusts}{unit}" }, "kind": { "clear": "맑음", @@ -2248,102 +2259,107 @@ } }, "healthMonitoring": { - "name": "", - "description": "", + "name": "시스템 상태 모니터링", + "description": "시스템의 상태 정보를 표시합니다.", "tab": { - "system": "", - "cluster": "" + "system": "시스템", + "cluster": "클러스터" }, "option": { "fahrenheit": { - "label": "" + "label": "CPU 온도 화씨 표시" }, "cpu": { - "label": "" + "label": "CPU 정보 표시" }, "memory": { - "label": "" + "label": "메모리 정보 표시" }, "gpu": { - "label": "" + "label": "GPU 정보 표시" }, "showUptime": { - "label": "" + "label": "작동 시간 표시" }, "fileSystem": { - "label": "" + "label": "파일시스템 정보 표시" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" }, "defaultTab": { - "label": "" + "label": "기본 탭" }, "visibleClusterSections": { - "label": "" + "label": "클러스터 섹션 시각화" }, "sectionIndicatorRequirement": { - "label": "" + "label": "섹션 표시기 요구사항" } }, "popover": { - "information": "", - "processor": "", - "memory": "", - "memoryAvailable": "", - "version": "", - "uptime": "", - "loadAverage": "", - "minute": "", - "minutes": "", - "used": "", - "available": "", - "lastSeen": "" + "information": "정보", + "processor": "프로세서: {cpuModelName}", + "memory": "메모리: {memory}GiB", + "memoryAvailable": "사용 가능: {memoryAvailable}GiB ({percent}%)", + "version": "버전: {version}", + "uptime": "작동 시간: {months}개월 {days}일 {hours}시간 {minutes}분", + "loadAverage": "평균 부하량:", + "minute": "1분", + "minutes": "{count}분", + "used": "사용중", + "available": "사용 가능", + "lastSeen": "마지막 상태 업데이트: {lastSeen}" }, "memory": {}, "error": { - "internalServerError": "" + "internalServerError": "상태 불러오기 실패" }, "cluster": { "summary": { - "cpu": "", - "memory": "" + "cpu": "CPU", + "memory": "RAM" }, "resource": { "node": { - "name": "" + "name": "노드" }, "qemu": { - "name": "" + "name": "가상머신" }, "lxc": { - "name": "" + "name": "LXC" }, "storage": { - "name": "" + "name": "저장 공간" } }, "popover": { "rightSection": { - "node": "", - "vmId": "", - "plugin": "" + "node": "노드", + "vmId": "가상머신 ID", + "plugin": "플러그인" }, "detail": { - "cpu": "", - "memory": "", - "storage": "", - "uptime": "", - "haState": "", + "cpu": "코어", + "memory": "메모리", + "storage": "저장 공간", + "uptime": "작동 시간", + "haState": "HA 상태", "storageType": { - "local": "", - "shared": "" + "local": "로컬 저장소", + "shared": "공유 저장소" } } }, "table": { "header": { - "name": "", - "cpu": "", - "memory": "", - "node": "" + "name": "이름", + "cpu": "CPU", + "memory": "RAM", + "node": "노드" } } } @@ -2352,6 +2368,9 @@ "name": "", "description": "", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "" }, @@ -2405,6 +2424,12 @@ "title": "" } }, + "vpn": { + "name": "VPN", + "description": "VPN 연결 개요", + "serviceUnavailable": "VPN 연결되지 않음", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2651,35 +2676,35 @@ } }, "mediaServer": { - "name": "", - "description": "", + "name": "현재 미디어 서버 스트림", + "description": "미디어 서버의 현재 스트림 표시", "option": { "showOnlyPlaying": { - "label": "", - "description": "" + "label": "현재 재생 중 항목만 표시", + "description": "비활성화 옵션은 plex에서는 지원되지 않음" } }, "items": { - "currentlyPlaying": "", - "user": "", - "name": "", - "id": "", + "currentlyPlaying": "현재 재생 중", + "user": "사용자", + "name": "이름", + "id": "아이디", "metadata": { - "title": "", + "title": "너드 통계", "video": { - "title": "", - "resolution": "" + "title": "동영상", + "resolution": "해상도" }, "audio": { - "title": "", - "channelCount": "", - "codec": "" + "title": "오디오", + "channelCount": "오디오 채널", + "codec": "오디오 코덱" }, "transcoding": { - "title": "", - "container": "", - "resolution": "", - "target": "" + "title": "트랜스코딩", + "container": "컨테이너", + "resolution": "해상도", + "target": "목표 코덱" } } } @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "", "description": "" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "", @@ -2776,133 +2805,133 @@ "noPhotos": "" }, "downloads": { - "name": "", - "description": "", + "name": "다운로드 클라이언트", + "description": "Torrent 및 Usenet 클라이언트 다운로드를 확인 및 관리할 수 있습니다.", "option": { "columns": { - "label": "" + "label": "표시할 열" }, "enableRowSorting": { - "label": "" + "label": "항목 정렬 활성화" }, "defaultSort": { - "label": "" + "label": "정렬 기준 열 기본값" }, "descendingDefaultSort": { - "label": "" + "label": "역순 정렬" }, "showCompletedUsenet": { - "label": "" + "label": "완료된 usenet 항목 표시" }, "showCompletedTorrent": { - "label": "" + "label": "완료된 torrent 항목 표시" }, "showCompletedHttp": { - "label": "" + "label": "완료된 기타 항목 표시" }, "activeTorrentThreshold": { "label": "" }, "categoryFilter": { - "label": "" + "label": "필터링할 카테고리/라벨" }, "filterIsWhitelist": { - "label": "" + "label": "화이트리스트로 필터링" }, "applyFilterToRatio": { - "label": "" + "label": "필터를 사용해 비율 계산" }, "limitPerIntegration": { - "label": "", - "description": "" + "label": "통합당 항목 수 제한", + "description": "통합당 표시되는 항목 수를 제한하여 전체 항목이 표시되지 않습니다" } }, "errors": { - "noColumns": "", - "noCommunications": "" + "noColumns": "항목에서 열 선택", + "noCommunications": "통합에서 데이터를 불러올 수 없습니다" }, "items": { "actions": { - "columnTitle": "" + "columnTitle": "제어" }, "added": { - "columnTitle": "", - "detailsTitle": "" + "columnTitle": "추가됨", + "detailsTitle": "추가된 날짜" }, "category": { - "columnTitle": "", - "detailsTitle": "" + "columnTitle": "기타", + "detailsTitle": "카테고리 (또는 기타 정보)" }, "downSpeed": { "columnTitle": "다운로드", "detailsTitle": "다운로드 속도" }, "index": { - "columnTitle": "", - "detailsTitle": "" + "columnTitle": "#", + "detailsTitle": "클라이언트 내 현재 인덱스" }, "id": { - "columnTitle": "" + "columnTitle": "아이디" }, "integration": { "columnTitle": "통합" }, "name": { - "columnTitle": "" + "columnTitle": "작업 이름" }, "progress": { "columnTitle": "진행률", - "detailsTitle": "" + "detailsTitle": "다운로드 진행률" }, "ratio": { - "columnTitle": "", - "detailsTitle": "" + "columnTitle": "비율", + "detailsTitle": "Torrent 비율 (수신/송신)" }, "received": { - "columnTitle": "", - "detailsTitle": "" + "columnTitle": "총 다운로드", + "detailsTitle": "전체 다운로드 용량" }, "sent": { - "columnTitle": "", - "detailsTitle": "" + "columnTitle": "총 업로드", + "detailsTitle": "전체 업로드 용량" }, "size": { - "columnTitle": "", - "detailsTitle": "" + "columnTitle": "파일 크기", + "detailsTitle": "선택/파일 총 크기" }, "state": { "columnTitle": "상태", - "detailsTitle": "" + "detailsTitle": "직업 상태" }, "time": { - "columnTitle": "", - "detailsTitle": "" + "columnTitle": "완료 시간", + "detailsTitle": "완료/완료 후 경과 시간" }, "type": { - "columnTitle": "", - "detailsTitle": "" + "columnTitle": "유형", + "detailsTitle": "다운로드 클라이언트 유형" }, "upSpeed": { "columnTitle": "업로드", - "detailsTitle": "" + "detailsTitle": "업로드 속도" } }, "states": { - "downloading": "", - "queued": "", + "downloading": "다운로드 중", + "queued": "대기 중", "paused": "일시 중지됨", "completed": "완료됨", - "failed": "", - "processing": "", - "leeching": "", - "stalled": "", + "failed": "실패", + "processing": "처리 중", + "leeching": "미완료", + "stalled": "정지됨", "unknown": "알 수 없음", - "seeding": "" + "seeding": "전송 중" }, "actions": { "clients": { - "modalTitle": "", - "pause": "", + "modalTitle": "다운로드 클라이언트 목록", + "pause": "모든 클라이언트/항목 정지", "resume": "" }, "client": { @@ -3133,6 +3162,9 @@ "provider": { "label": "" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "", "placeholder": "" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "", "tooltip": "" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "", "noMatchingVersion": "", "noReleasesFound": "", - "noProviderSeleceted": "", - "noProviderResponse": "" + "noProviderSelected": "", + "noProviderResponse": "", + "unexpected": "" } } }, @@ -3211,7 +3251,7 @@ "option": {}, "card": { "vpn": { - "countConnected": "" + "countConnected": "{count}개 연결" } }, "error": { @@ -3293,9 +3333,9 @@ } }, "notifications": { - "name": "", - "description": "", - "noItems": "", + "name": "알림", + "description": "통합에서 알림 기록 표시", + "noItems": "표시할 알림이 없습니다.", "option": {} }, "speedtestTracker": { @@ -3338,11 +3378,11 @@ } }, "uptimeKuma": { - "name": "", - "description": "", + "name": "Uptime Kuma", + "description": "Uptime Kuma 상태 페이지에서 업타임 통계를 모니터링합니다", "option": { "showAverageUptime": { - "label": "", + "label": "평균 업타임 표시", "description": "" }, "showUptimeRing": { @@ -3445,68 +3485,73 @@ } }, "systemResources": { - "name": "", - "description": "", + "name": "시스템 리소스", + "description": "시스템의 CPU, 메모리, 디스크 및 기타 하드웨어 사용량", "option": { "hasShadow": { - "label": "" + "label": "차트 음영 사용" }, "visibleCharts": { - "label": "", - "description": "", + "label": "표시할 차트", + "description": "표시할 차트를 선택합니다.", "option": { - "cpu": "", - "memory": "", - "gpu": "", - "network": "" + "cpu": "CPU", + "memory": "메모리", + "gpu": "GPU", + "network": "네트워크" } }, "labelDisplayMode": { - "label": "", + "label": "라벨 표시 모드", "option": { - "textWithIcon": "", - "text": "", - "icon": "", - "hidden": "" + "textWithIcon": "텍스트 및 아이콘 표시", + "text": "텍스트만 표시", + "icon": "아이콘만 표시", + "hidden": "라벨 숨기기" } } }, "card": { - "cpu": "", - "memory": "", - "gpu": "", - "network": "", - "up": "", - "down": "" + "cpu": "CPU", + "memory": "메모리", + "gpu": "GPU", + "network": "네트워크", + "up": "업로드", + "down": "다운로드" } }, "systemDisks": { - "name": "", - "description": "", + "name": "시스템 디스크", + "description": "시스템의 디스크 사용량", "option": { "showTemperatureIfAvailable": { - "label": "" + "label": "가능한 경우 온도 표시" }, "displayMode": { - "label": "", + "label": "디스플레이 모드", "option": { "percentage": { - "label": "" + "label": "퍼센트 (예: 76%)" }, "absolute": { - "label": "" + "label": "절대값 (예: 800GB / 1TB)" }, "free": { - "label": "" + "label": "여유 공간 퍼센트 (예: 24% 남음)" } } }, "showBackgroundBar": { - "label": "" + "label": "백그라운드 사용량 바 표시" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { - "unhealthy": "" + "unhealthy": "비정상" } }, "customApi": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -3884,7 +3935,7 @@ "placeholder": "로컬 이미지 검색을 위한 타이핑 시작", "group": { "your": "내 이미지", - "other": "" + "other": "기타 이미지" } }, "backgroundImageAttachment": { @@ -3922,7 +3973,7 @@ } }, "backgroundImageSize": { - "label": "", + "label": "배경 이미지 크기", "option": { "cover": { "label": "", @@ -3941,38 +3992,38 @@ "label": "보조 색상" }, "opacity": { - "label": "" + "label": "불투명도" }, "iconColor": { - "label": "" + "label": "아이콘 색상" }, "clearColor": { - "label": "" + "label": "색상 제거" }, "customCss": { - "label": "", + "label": "이 보드에 적용할 사용자 지정 css", "description": "또한 숙련된 사용자에게만 권장되는 CSS를 사용하여 대시보드를 사용자 지정할 수 있습니다.", "customClassesAlert": { - "title": "", + "title": "사용자 지정 클래스", "description": "" } }, "disableStatus": { - "label": "", - "description": "" + "label": "앱 상태 비활성화", + "description": "이 보드의 모든 앱에 대한 상태 확인을 비활성화합니다" }, "columnCount": { - "label": "" + "label": "열 개수" }, "itemRadius": { - "label": "", - "description": "", + "label": "항목 둥글기", + "description": "보드에 있는 타일의 모서리 둥글기를 변경합니다", "option": { - "xs": "", - "sm": "", - "md": "", - "lg": "", - "xl": "" + "xs": "매우 작게", + "sm": "작게", + "md": "보통", + "lg": "크게", + "xl": "매우 크게" } }, "name": { @@ -3980,25 +4031,25 @@ }, "isPublic": { "label": "공개", - "description": "" + "description": "공개 게시판은 계정이 없어도 누구나 접근할 수 있습니다." } }, "content": { "metaTitle": "" }, "setting": { - "title": "", + "title": "{boardName} 보드 설정", "section": { "general": { "title": "일반", - "unrecognizedLink": "" + "unrecognizedLink": "제공된 링크가 인식되지 않아 미리보기할 수 없습니다, 다만 작동할 수도 있습니다." }, "layout": { "title": "레이아웃", "responsive": { - "title": "", + "title": "반응형 레이아웃", "action": { - "add": "" + "add": "레이아웃 추가" } } }, @@ -4006,10 +4057,10 @@ "title": "배경" }, "appearance": { - "title": "" + "title": "모양" }, "customCss": { - "title": "" + "title": "사용자 지정 css" }, "behavior": { "title": "동작" @@ -4133,6 +4184,11 @@ "title": "찾을 수 없음", "text": "요청한 리소스를 찾을 수 없습니다" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "홈", @@ -4499,86 +4555,14 @@ "disabled": "" }, "job": { - "minecraftServerStatus": { - "label": "" - }, - "timetable": { - "label": "" - }, "iconsUpdater": { "label": "" }, "analytics": { "label": "" }, - "smartHomeEntityState": { - "label": "" - }, "ping": { "label": "" - }, - "mediaServer": { - "label": "미디어 서버" - }, - "mediaOrganizer": { - "label": "" - }, - "downloads": { - "label": "" - }, - "mediaRequestStats": { - "label": "" - }, - "mediaRequestList": { - "label": "" - }, - "rssFeeds": { - "label": "" - }, - "indexerManager": { - "label": "" - }, - "healthMonitoring": { - "label": "" - }, - "dnsHole": { - "label": "" - }, - "updateChecker": { - "label": "" - }, - "mediaTranscoding": { - "label": "" - }, - "networkController": { - "label": "" - }, - "refreshNotifications": { - "label": "" - }, - "dockerContainers": { - "label": "" - }, - "firewallCpu": { - "label": "" - }, - "firewallMemory": { - "label": "" - }, - "firewallVersion": { - "label": "" - }, - "firewallInterfaces": { - "label": "" - }, - "weather": { - "label": "" - }, - "tracearr": { - "label": "" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -5041,7 +5025,7 @@ } }, "refresh": { - "label": "", + "label": "새로고침", "notification": { "success": { "title": "", @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "" + "internalServerError": "", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/lt.json b/packages/translation/src/lang/lt.json index afec241f1..f3f3fce9b 100644 --- a/packages/translation/src/lang/lt.json +++ b/packages/translation/src/lang/lt.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "", "speedtest": "", "analytics": "", + "vpn": "", "mediaLibrary": "", "uptimeMonitoring": "" }, @@ -1412,7 +1413,10 @@ "here": "", "iconPicker": { "label": "", - "header": "" + "header": "", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "", "switchToLightMode": "", "management": "", + "docker": "", "preferences": "", "logout": "", "login": "Prisijungti", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "Nustatymai" + "settings": "Nustatymai", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "", "noStation": "", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -2352,6 +2368,9 @@ "name": "", "description": "", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "" }, @@ -2405,6 +2424,12 @@ "title": "" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "", "description": "" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "", @@ -3133,6 +3162,9 @@ "provider": { "label": "" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "", "placeholder": "" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "", "tooltip": "" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "", "noMatchingVersion": "", "noReleasesFound": "", - "noProviderSeleceted": "", - "noProviderResponse": "" + "noProviderSelected": "", + "noProviderResponse": "", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "", "text": "" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "Pagrindinis", @@ -4499,86 +4555,14 @@ "disabled": "" }, "job": { - "minecraftServerStatus": { - "label": "" - }, - "timetable": { - "label": "" - }, "iconsUpdater": { "label": "" }, "analytics": { "label": "" }, - "smartHomeEntityState": { - "label": "" - }, "ping": { "label": "" - }, - "mediaServer": { - "label": "Medijų serveris" - }, - "mediaOrganizer": { - "label": "" - }, - "downloads": { - "label": "" - }, - "mediaRequestStats": { - "label": "" - }, - "mediaRequestList": { - "label": "" - }, - "rssFeeds": { - "label": "" - }, - "indexerManager": { - "label": "" - }, - "healthMonitoring": { - "label": "" - }, - "dnsHole": { - "label": "" - }, - "updateChecker": { - "label": "" - }, - "mediaTranscoding": { - "label": "" - }, - "networkController": { - "label": "" - }, - "refreshNotifications": { - "label": "" - }, - "dockerContainers": { - "label": "" - }, - "firewallCpu": { - "label": "" - }, - "firewallMemory": { - "label": "" - }, - "firewallVersion": { - "label": "" - }, - "firewallInterfaces": { - "label": "" - }, - "weather": { - "label": "" - }, - "tracearr": { - "label": "" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "" + "internalServerError": "", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/lv.json b/packages/translation/src/lang/lv.json index e5d262048..2685a3b60 100644 --- a/packages/translation/src/lang/lv.json +++ b/packages/translation/src/lang/lv.json @@ -6,9 +6,9 @@ "subtitle": "", "description": "", "action": { - "scratch": "", + "scratch": "Sākt no jauna", "importOldmarr": "", - "restoreSqlite": "", + "restoreSqlite": "SQLite rezerves kopija", "restoreSqliteDescription": "", "backToOptions": "" } @@ -26,19 +26,19 @@ } }, "importSettings": { - "title": "", + "title": "Importēt iestatījumus", "description": "" }, "boardSelection": { "title": "", "description": "", "action": { - "selectAll": "", + "selectAll": "Atzīmēt visu", "unselectAll": "" } }, "summary": { - "title": "", + "title": "Importēšanas kopsavilkums", "description": "", "action": { "import": "" @@ -46,7 +46,7 @@ "entities": { "apps": "Lietotnes", "boards": "Dēļi", - "integrations": "", + "integrations": "Integrācijas", "credentialUsers": "" } }, @@ -67,15 +67,15 @@ } }, "user": { - "title": "", + "title": "Administratora lietotājs", "subtitle": "", "notification": { "success": { - "title": "", + "title": "Lietotājs izveidots", "message": "" }, "error": { - "title": "" + "title": "Lietotāja izveide neizdevās" } } }, @@ -94,19 +94,19 @@ "subtitle": "" }, "integrations": { - "title": "", + "title": "Integrācijas", "subtitle": "", "description": "", "action": { - "continue": "", - "skip": "" + "continue": "Turpināt", + "skip": "Pagaidām izlaist šo soli" }, "configure": { - "progress": "", - "skip": "", + "progress": "{current} no {total}", + "skip": "Izlaist šo integrāciju", "finishing": "", "error": { - "title": "", + "title": "Neizdevās iestatīt!", "message": "" }, "helpers": { @@ -115,7 +115,7 @@ } }, "urlTemplate": { - "label": "", + "label": "Bāzes URL", "placeholder": "", "hint": "", "warning": "", @@ -136,14 +136,14 @@ } }, "finish": { - "title": "", + "title": "Pabeigt Iestatīšanu", "subtitle": "", "description": "", "action": { "goToBoard": "", "createBoard": "", - "inviteUser": "", - "docs": "" + "inviteUser": "Ielūgt citus lietotājus", + "docs": "Lasīt dokumentāciju" } } }, @@ -158,7 +158,7 @@ "subtitle": "" }, "invite": { - "title": "", + "title": "Pievienoties Homarr", "subtitle": "", "description": "" }, @@ -177,20 +177,20 @@ }, "password": { "label": "Parole", - "suggestionsTitle": "", + "suggestionsTitle": "Paroles ieteikumi", "suggestion": { - "length": "", + "length": "Vismaz 8 rakstzīmes", "lowercase": "Ietver mazo burtu", "uppercase": "Ietver lielo burtu", "number": "Ietver numuru", - "special": "" + "special": "Iekļauj speciālo simbolu" } }, "passwordConfirm": { "label": "Apstipriniet paroli" }, "previousPassword": { - "label": "" + "label": "Iepriekšējā parole" }, "homeBoard": { "label": "" @@ -218,10 +218,10 @@ "notification": { "success": { "title": "", - "message": "" + "message": "Jūs esat veiksmīgi pieteicies" }, "error": { - "title": "", + "title": "Pieteikšanās neizdevās", "message": "" } }, @@ -245,13 +245,13 @@ }, "create": "Izveidot lietotāju", "changePassword": { - "label": "", + "label": "Mainīt paroli", "notification": { "success": { - "message": "" + "message": "Parole veiksmīgi nomainīta." }, "error": { - "message": "" + "message": "Nav iespējams nomainīt paroli" } } }, @@ -595,12 +595,12 @@ } }, "defaultGroup": { - "name": "", + "name": "Noklusētā grupa", "description": "" } }, "app": { - "search": "", + "search": "Atrast aplikāciju", "page": { "list": { "title": "Lietotnes", @@ -689,7 +689,7 @@ "list": { "title": "", "noResults": "", - "loading": "" + "loading": "Notiek ielāde..." }, "create": { "title": "", @@ -700,7 +700,7 @@ } }, "action": { - "create": "", + "create": "Izveidot", "import": "", "save": "", "delete": "", @@ -1021,6 +1021,7 @@ "mediaMonitoring": "", "speedtest": "", "analytics": "", + "vpn": "", "mediaLibrary": "", "uptimeMonitoring": "" }, @@ -1412,7 +1413,10 @@ "here": "", "iconPicker": { "label": "", - "header": "" + "header": "", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "", "switchToLightMode": "", "management": "", + "docker": "", "preferences": "Jūsu iestatījumi", "logout": "", "login": "Pieslēgties", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "Iestatījumi" + "settings": "Iestatījumi", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "", "noStation": "", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "Rādīt failu sistēmas informāciju" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -2352,6 +2368,9 @@ "name": "", "description": "", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "" }, @@ -2405,6 +2424,12 @@ "title": "" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "", "description": "" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "", @@ -3133,6 +3162,9 @@ "provider": { "label": "" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "", "placeholder": "" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "", "tooltip": "" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "", "noMatchingVersion": "", "noReleasesFound": "", - "noProviderSeleceted": "", - "noProviderResponse": "" + "noProviderSelected": "", + "noProviderResponse": "", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "", "text": "" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "Sākums", @@ -4499,86 +4555,14 @@ "disabled": "" }, "job": { - "minecraftServerStatus": { - "label": "" - }, - "timetable": { - "label": "" - }, "iconsUpdater": { "label": "" }, "analytics": { "label": "" }, - "smartHomeEntityState": { - "label": "" - }, "ping": { "label": "" - }, - "mediaServer": { - "label": "Multivides Serveris" - }, - "mediaOrganizer": { - "label": "" - }, - "downloads": { - "label": "" - }, - "mediaRequestStats": { - "label": "" - }, - "mediaRequestList": { - "label": "" - }, - "rssFeeds": { - "label": "" - }, - "indexerManager": { - "label": "" - }, - "healthMonitoring": { - "label": "" - }, - "dnsHole": { - "label": "" - }, - "updateChecker": { - "label": "" - }, - "mediaTranscoding": { - "label": "" - }, - "networkController": { - "label": "" - }, - "refreshNotifications": { - "label": "" - }, - "dockerContainers": { - "label": "" - }, - "firewallCpu": { - "label": "" - }, - "firewallMemory": { - "label": "" - }, - "firewallVersion": { - "label": "" - }, - "firewallInterfaces": { - "label": "" - }, - "weather": { - "label": "" - }, - "tracearr": { - "label": "" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "" + "internalServerError": "", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/nl.json b/packages/translation/src/lang/nl.json index 66a8f48d1..7d1e94f4d 100644 --- a/packages/translation/src/lang/nl.json +++ b/packages/translation/src/lang/nl.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "", "speedtest": "", "analytics": "", + "vpn": "", "mediaLibrary": "", "uptimeMonitoring": "" }, @@ -1412,7 +1413,10 @@ "here": "hier", "iconPicker": { "label": "Icoon-URL", - "header": "Typ naam of objecten om te filteren op iconen… Homarr doorzoekt {countIcons} iconen voor je." + "header": "Typ naam of objecten om te filteren op iconen… Homarr doorzoekt {countIcons} iconen voor je.", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "Schakel naar donkere modus", "switchToLightMode": "Schakel naar lichte modus", "management": "Beheer", + "docker": "", "preferences": "Jouw voorkeuren", "logout": "Uitloggen", "login": "Inloggen", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "Instellingen" + "settings": "Instellingen", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "", "noStation": "", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "Bestandssysteem info weergeven" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Standaard tab" }, @@ -2352,6 +2368,9 @@ "name": "Docker statistieken", "description": "", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "" }, @@ -2405,6 +2424,12 @@ "title": "" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "", "description": "" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "", @@ -3133,6 +3162,9 @@ "provider": { "label": "Aanbieder" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "Identificator", "placeholder": "Naam of Eigenaar/naam" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "Geen aanbieder", "tooltip": "" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "Ongeldige identificator", "noMatchingVersion": "Geen overeenkomende versie gevonden", "noReleasesFound": "Geen uitgaves gevonden", - "noProviderSeleceted": "Geen aanbieder geselecteerd", - "noProviderResponse": "Geen reactie van aanbieder" + "noProviderSelected": "", + "noProviderResponse": "Geen reactie van aanbieder", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "Niet gevonden", "text": "Kon de aangevraagde bron niet vinden" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "Home", @@ -4499,86 +4555,14 @@ "disabled": "Uitgeschakeld" }, "job": { - "minecraftServerStatus": { - "label": "Minecraft server status" - }, - "timetable": { - "label": "" - }, "iconsUpdater": { "label": "Icoon updater" }, "analytics": { "label": "Analytics" }, - "smartHomeEntityState": { - "label": "Smart home entiteitstatus" - }, "ping": { "label": "Pings" - }, - "mediaServer": { - "label": "Mediaserver" - }, - "mediaOrganizer": { - "label": "Media organisatoren" - }, - "downloads": { - "label": "Downloads" - }, - "mediaRequestStats": { - "label": "Media-aanvraag statistieken" - }, - "mediaRequestList": { - "label": "Media-aanvraag lijst" - }, - "rssFeeds": { - "label": "RSS-feeds" - }, - "indexerManager": { - "label": "Indexeringsbeheerder" - }, - "healthMonitoring": { - "label": "Gezondheidsmonitoring" - }, - "dnsHole": { - "label": "DNS-hole gegevens" - }, - "updateChecker": { - "label": "Update checker" - }, - "mediaTranscoding": { - "label": "Mediatranscodering" - }, - "networkController": { - "label": "Netwerkcontroller" - }, - "refreshNotifications": { - "label": "Melding updater" - }, - "dockerContainers": { - "label": "Docker containers" - }, - "firewallCpu": { - "label": "Firewall CPU" - }, - "firewallMemory": { - "label": "Firewall geheugen" - }, - "firewallVersion": { - "label": "Firewall versie" - }, - "firewallInterfaces": { - "label": "Firewall interfaces" - }, - "weather": { - "label": "Weer" - }, - "tracearr": { - "label": "" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "Docker-containers zijn niet opgehaald" + "internalServerError": "Docker-containers zijn niet opgehaald", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/no.json b/packages/translation/src/lang/no.json index 00309d122..595f6b71e 100644 --- a/packages/translation/src/lang/no.json +++ b/packages/translation/src/lang/no.json @@ -8,8 +8,8 @@ "action": { "scratch": "Start fra begynnelsen", "importOldmarr": "Importer fra Homarr før 1.0", - "restoreSqlite": "", - "restoreSqliteDescription": "", + "restoreSqlite": "Gjenopprett fra SQLite sikkerhetskopi", + "restoreSqliteDescription": "Last opp en full SQLite database sikkerhetskopi fra en tidligere Homarr instans. Dette vil gjenopprette alle tavler, brukere, apper, integrasjoner og innstillinger.", "backToOptions": "" } }, @@ -1021,6 +1021,7 @@ "mediaMonitoring": "", "speedtest": "", "analytics": "", + "vpn": "", "mediaLibrary": "", "uptimeMonitoring": "" }, @@ -1412,7 +1413,10 @@ "here": "her", "iconPicker": { "label": "Ikon-URL", - "header": "Skriv navn eller objekt å filtrere for ikoner... Homarr vil søke gjennom {countIcons} ikoner for deg." + "header": "Skriv navn eller objekt å filtrere for ikoner... Homarr vil søke gjennom {countIcons} ikoner for deg.", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "Bytt til mørk modus", "switchToLightMode": "Bytt til lys modus", "management": "Administrasjon", + "docker": "", "preferences": "Dine innstillinger", "logout": "Logg ut", "login": "Logg Inn", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "Innstillinger" + "settings": "Innstillinger", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "Avganger {station}", "noStation": "Ingen stasjon valgt", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "Stasjon" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "Vis filsysteminfo" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Standard fane" }, @@ -2352,6 +2368,9 @@ "name": "Docker status", "description": "Statistikk over containerne dine (denne widgeten kan bare legges til med administratorrettigheter)", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "Aktiver sortering av elementer" }, @@ -2405,6 +2424,12 @@ "title": "Instanser" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "Vis bildeinformasjon", "description": "Vis datoen for bildet og gjeldende indeks nederst i lysbildeserien" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "Lysbildefremvisning av bilder i ditt Immich-album", @@ -3133,6 +3162,9 @@ "provider": { "label": "Leverandør" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "Identifikator", "placeholder": "Navn eller Eier/Navn" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "Ingen leverandør", "tooltip": "Leverandøren kunne ikke tolkes, sett den manuelt etter import av bildene" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "Ugyldig identifikator", "noMatchingVersion": "Ingen samsvarende versjon funnet", "noReleasesFound": "Ingen utgivelser funnet", - "noProviderSeleceted": "Ingen leverandør valgt", - "noProviderResponse": "Ingen respons fra leverandør" + "noProviderSelected": "", + "noProviderResponse": "Ingen respons fra leverandør", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "Vis bakgrunnsbrukslinje" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "Ikke funnet", "text": "Fant ikke den forespurte ressursen" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "Hjem", @@ -4499,86 +4555,14 @@ "disabled": "Deaktivert" }, "job": { - "minecraftServerStatus": { - "label": "Minecraft serverstatus" - }, - "timetable": { - "label": "Timeplan" - }, "iconsUpdater": { "label": "Ikon oppdatering" }, "analytics": { "label": "Statistikk" }, - "smartHomeEntityState": { - "label": "Smart hjem enhets tilstand" - }, "ping": { "label": "Pinger" - }, - "mediaServer": { - "label": "Medieserver" - }, - "mediaOrganizer": { - "label": "Media organisatorer" - }, - "downloads": { - "label": "Nedlastinger" - }, - "mediaRequestStats": { - "label": "Statistikk for mediaforespørsler" - }, - "mediaRequestList": { - "label": "Liste over mediaspillforespørsler" - }, - "rssFeeds": { - "label": "RSS feed" - }, - "indexerManager": { - "label": "Indekserings-behandler" - }, - "healthMonitoring": { - "label": "Systemhelseovervåking" - }, - "dnsHole": { - "label": "DNS Hole Data" - }, - "updateChecker": { - "label": "Oppdateringssjekk" - }, - "mediaTranscoding": { - "label": "Media transkoding" - }, - "networkController": { - "label": "Nettverkskontroller" - }, - "refreshNotifications": { - "label": "Varseloppdaterer" - }, - "dockerContainers": { - "label": "Docker-beholdere" - }, - "firewallCpu": { - "label": "Brannmur-CPU" - }, - "firewallMemory": { - "label": "Brannmur-minne" - }, - "firewallVersion": { - "label": "Brannmur-versjon" - }, - "firewallInterfaces": { - "label": "Brannmur-grensesnitt" - }, - "weather": { - "label": "Vær" - }, - "tracearr": { - "label": "Tracearr" - }, - "umamiActiveVisitors": { - "label": "Umami Aktive Besøkende" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "Klarte ikke hente Docker-containere" + "internalServerError": "Klarte ikke hente Docker-containere", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/pl.json b/packages/translation/src/lang/pl.json index 90aa8c3eb..31d301ceb 100644 --- a/packages/translation/src/lang/pl.json +++ b/packages/translation/src/lang/pl.json @@ -2,15 +2,15 @@ "init": { "step": { "start": { - "title": "Witaj w Homarrze", - "subtitle": "Zacznijmy od konfiguracji twojego Homarra.", + "title": "Witaj w Homarr", + "subtitle": "Zacznijmy od konfiguracji Twojej instancji Homarr.", "description": "Aby rozpocząć, wybierz jak chcesz skonfigurować instancję Homarr.", "action": { - "scratch": "Zacznij od nowa", + "scratch": "Zacznij od zera", "importOldmarr": "Importuj z Homarr przed 1.0", - "restoreSqlite": "", - "restoreSqliteDescription": "", - "backToOptions": "" + "restoreSqlite": "Przywróć z kopii zapasowej SQLite", + "restoreSqliteDescription": "Prześlij pełną kopię zapasową bazy danych SQLite z poprzedniej instancji Homarr. Spowoduje to przywrócenie wszystkich tablic, użytkowników, aplikacji, integracji i ustawień.", + "backToOptions": "← Wróć do opcji konfiguracji" } }, "import": { @@ -31,7 +31,7 @@ }, "boardSelection": { "title": "Znaleziono {count} tablic", - "description": "Wybierz wszystkie działy o rozmiarze do zaimportowania", + "description": "Wybierz wszystkie tablice o rozmiarze do zaimportowania", "action": { "selectAll": "Zaznacz wszystkie", "unselectAll": "Odznacz wszystkie" @@ -55,7 +55,7 @@ "field": { "token": { "label": "Token", - "description": "Wprowadź token importu z poprzedniej instancji hommara" + "description": "Wprowadź token importu z poprzedniej instancji homarr" } }, "notification": { @@ -94,45 +94,45 @@ "subtitle": "Konfiguruj ustawienia serwera" }, "integrations": { - "title": "", - "subtitle": "", - "description": "", + "title": "Integracje", + "subtitle": "Wybierz usługi, których używasz.", + "description": "Wybierz integracje, które chcesz połączyć. Skonfigurujemy Twoją tablicę z pasującymi widgetami automatycznie.", "action": { - "continue": "", - "skip": "" + "continue": "Kontynuuj", + "skip": "Pomiń na razie" }, "configure": { - "progress": "", - "skip": "", - "finishing": "", + "progress": "{current} z {total}", + "skip": "Pomiń tę integrację", + "finishing": "Konfigurowanie twojej tablicy...", "error": { - "title": "", - "message": "" + "title": "Konfiguracja nie powiodła się", + "message": "Coś poszło nie tak podczas konfiguracji twojej tablicy. Spróbuj ponownie później." }, "helpers": { - "tooltip": "", - "description": "" + "tooltip": "URL helpers", + "description": "URL helpers spróbują odgadnąć URL twoich serwisów. Jeżeli to nie zadziała, po prostu wpisz adres ręcznie." } }, "urlTemplate": { - "label": "", - "placeholder": "", - "hint": "", - "warning": "", - "preview": "", + "label": "Base URL", + "placeholder": "homelab.local lub 192.168.1.1", + "hint": "Wprowadź swoją podstawową domenę lub adres IP, aby automatycznie wypełniać adresy URL integracji", + "warning": "Ustawienie Base URL spowoduje automatyczne wypełnienie wszystkich adresów URL integracji, oszczędzając czas podczas konfiguracji", + "preview": "np. {example}", "mode": { - "subdomain": "", - "hostPort": "" + "subdomain": "Subdomena", + "hostPort": "Host:Port" } }, "docker": { - "detected": "", - "detectedBreakdown": "", - "summary": "", - "appBadge": "", - "appSectionLabel": "", - "scanning": "", - "scanningDescription": "" + "detected": "Doker wykrył {count} usług", + "detectedBreakdown": "Integracje {integrations} · {apps} aplikacje", + "summary": "Homarr wykrył {apps} aplikacji, z których {integrations} ma wbudowane integracje", + "appBadge": "Aplikacja", + "appSectionLabel": "Aplikacje do dodania do tablicy (wybrano {count})", + "scanning": "Skanowanie Dockera...", + "scanningDescription": "Szukanie uruchomionych kontenerów w systemie" } }, "finish": { @@ -143,7 +143,7 @@ "goToBoard": "Idź do tablicy {name}", "createBoard": "Stwórz swoją pierwszą tablicę", "inviteUser": "Zaproś innych użytkowników", - "docs": "Przeczytaj dokumentację!" + "docs": "Przeczytaj dokumentację" } } }, @@ -163,7 +163,7 @@ "description": "Zostałeś(aś) zaproszony(a) przez {username}" }, "init": { - "title": "Nowa instalacja Homarra", + "title": "Nowa instalacja Homarr", "subtitle": "Proszę utworzyć użytkownika dla podstawowego administratora" } }, @@ -177,7 +177,7 @@ }, "password": { "label": "Hasło", - "suggestionsTitle": "", + "suggestionsTitle": "Propozycje hasła", "suggestion": { "length": "Zawiera co najmniej 8 znaków", "lowercase": "Zawiera małą literę", @@ -205,7 +205,7 @@ "label": "Otwórz wyniki wyszukiwania w nowej karcie" }, "ddgBangs": { - "label": "" + "label": "Włącz DuckDuckGo bangs" } }, "error": { @@ -239,11 +239,11 @@ }, "error": { "title": "Tworzenie konta nie powiodło się", - "message": "Twoje konto nie może zostać utworzone" + "message": "Twoje konto nie mogło zostać utworzone" } } }, - "create": "Dodaj użytkownika", + "create": "Utwórz użytkownika", "changePassword": { "label": "Zmień hasło", "notification": { @@ -251,7 +251,7 @@ "message": "Hasło zostało pomyślnie zmienione" }, "error": { - "message": "Problem ze zmianą hasła" + "message": "Nie można zmienić hasła" } } }, @@ -268,7 +268,7 @@ "changeSearchPreferences": { "notification": { "success": { - "message": "Ustawienia wyszukiwania zostały zmienione" + "message": "Preferencje wyszukiwania zostały zmienione" }, "error": { "message": "Nie można zmienić preferencji wyszukiwania" @@ -288,20 +288,20 @@ "changePingIconsEnabled": { "notification": { "success": { - "message": "Włączono ping na ikonach" + "message": "Pomyślnie przełączono ping ikon" }, "error": { - "message": "Nie można włączyć pingowania ikon" + "message": "Nie można przełączyć ping ikon" } } }, "changeDdgBangs": { "notification": { "success": { - "message": "" + "message": "DuckDuckGo bangs zostały pomyślnie przełączone" }, "error": { - "message": "" + "message": "Nie można przełączyć DuckDuckGo bangs" } } }, @@ -376,7 +376,7 @@ }, "permission": { "admin": { - "title": "Admin", + "title": "Administrator", "item": { "admin": { "label": "Administrator", @@ -451,16 +451,16 @@ "title": "Multimedia", "item": { "upload": { - "label": "Prześlij media", - "description": "Zezwalaj użytkownikom na przesyłanie mediów" + "label": "Prześlij multimedia", + "description": "Zezwalaj użytkownikom na przesyłanie multimediów" }, "view-all": { - "label": "Wyświetl wszystkie media", - "description": "Zezwalaj użytkownikom na wyświetlanie wszystkich mediów" + "label": "Wyświetl wszystkie multimedia", + "description": "Zezwalaj użytkownikom na wyświetlanie wszystkich multimediów" }, "full-all": { - "label": "Pełny dostęp do mediów", - "description": "Zezwalaj użytkownikom na zarządzanie i usuwanie wszelkich mediów" + "label": "Pełny dostęp do multimediów", + "description": "Zezwalaj użytkownikom na zarządzanie i usuwanie wszelkich multimediów" } } }, @@ -513,13 +513,13 @@ "transfer": { "label": "Przenieś własność", "description": "Przenieś właściciela tej grupy na innego użytkownika.", - "confirm": "Czy na pewno chcesz przenieść właściciela grupy {name} na {username}?", + "confirm": "Czy na pewno chcesz zmienić właściciela grupy {name} na {username}?", "notification": { "success": { - "message": "Przeniesiono pomyślnie właściciela grupy {group} do {user} " + "message": "Pomyślnie zmieniono właściciela grupy {group} na {user}" }, "error": { - "message": "Nie można przenieść właściciela" + "message": "Nie można zmienić właściciela" } } }, @@ -527,7 +527,7 @@ "label": "Dodaj członka" }, "removeMember": { - "label": "Usuń użytkownika", + "label": "Usuń członka", "confirm": "Czy na pewno chcesz usunąć {user} z tej grupy?" }, "delete": { @@ -536,7 +536,7 @@ "confirm": "Czy na pewno chcesz usunąć grupę {name}?", "notification": { "success": { - "message": "Usunięto grupę {name}" + "message": "Pomyślnie usunięto grupę {name}" }, "error": { "message": "Nie udało się usunąć grupy {name}" @@ -687,213 +687,213 @@ "customWidget": { "page": { "list": { - "title": "", - "noResults": "", - "loading": "" + "title": "Niestandardowe widgety", + "noResults": "Brak niestandardowych widgetów. Utwórz swój pierwszy!", + "loading": "Ładowanie..." }, "create": { - "title": "", - "subtitle": "" + "title": "Utwórz niestandardowy widget", + "subtitle": "Zdefiniuj adres URL API, uwierzytelnianie oraz sposób wyświetlania danych z odpowiedzi." }, "edit": { - "title": "" + "title": "Edytuj niestandardowy widget" } }, "action": { - "create": "", - "import": "", - "save": "", - "delete": "", - "deleteConfirm": "", - "export": "", - "addMapping": "", - "addColumn": "", - "edit": "", - "removeMapping": "", - "removeColumn": "", - "addItem": "", - "removeItem": "", - "addBar": "", - "duplicate": "", - "copyAiPrompt": "", - "enable": "", - "disable": "", - "menu": "" + "create": "Utwórz", + "import": "Importuj", + "save": "Zapisz", + "delete": "Usuń niestandardowy widget", + "deleteConfirm": "Czy na pewno chcesz usunąć \"{name}\"? Tej akcji nie można cofnąć.", + "export": "Eksportuj", + "addMapping": "Dodaj mapowanie", + "addColumn": "Dodaj kolumnę", + "edit": "Edytuj", + "removeMapping": "Usuń mapowanie", + "removeColumn": "Usuń kolumnę", + "addItem": "Dodaj element", + "removeItem": "Usuń element", + "addBar": "Dodaj pasek", + "duplicate": "Duplikuj", + "copyAiPrompt": "Skopiuj prompt AI", + "enable": "Włącz", + "disable": "Wyłącz", + "menu": "Akcje" }, "field": { - "name": "", - "description": "", - "iconUrl": "", - "url": "", - "authType": "", - "headerName": "", - "method": "", - "requestBody": "", - "displayType": "", - "jsonPath": "", - "jsonPathHint": "", - "label": "", - "unit": "", - "mappings": "", - "tablePath": "", - "tablePathHint": "", - "columns": "", - "header": "", - "items": "", - "gridColumns": "", - "cardStyle": "", - "bars": "", - "valuePath": "", - "maxPath": "", - "showPercentage": "", - "barSize": "", - "goodValues": "", - "dotSize": "", - "valueSize": "", - "labelPosition": "", - "kvLayout": "", - "striped": "", - "compact": "", - "maxHeight": "", - "color": "", - "buttonLabel": "", - "buttonColor": "", - "confirmText": "", - "confirmTextHint": "", - "successMessage": "", - "successMessageHint": "", - "enabled": "", + "name": "Nazwa", + "description": "Opis", + "iconUrl": "Adres URL ikony", + "url": "Adres URL", + "authType": "Typ uwierzytelniania", + "headerName": "Nazwa parametru Header/Query", + "method": "Metoda", + "requestBody": "Body Zapytania", + "displayType": "Typ Wyświetlania", + "jsonPath": "Ścieżka JSON", + "jsonPathHint": "Użyj składni JSONPath do wyodrębnienia wartości, np. $.data.temperature lub $.items[0].name", + "label": "Etykieta", + "unit": "Jednostka", + "mappings": "Mapowania", + "tablePath": "Ścieżka JSON tablicy", + "tablePathHint": "Ścieżka do tablicy w odpowiedzi, np. $.data.items", + "columns": "Kolumny", + "header": "Nagłówek", + "items": "Elementy", + "gridColumns": "Kolumny Siatki", + "cardStyle": "Styl Karty", + "bars": "Paski", + "valuePath": "Ścieżka JSON wartości", + "maxPath": "Ścieżka JSON wartości maksymalnej", + "showPercentage": "Pokaż Procent", + "barSize": "Rozmiar Paska", + "goodValues": "Dobre wartości (oddzielone przecinkami)", + "dotSize": "Rozmiar Kropki", + "valueSize": "Rozmair Wartości", + "labelPosition": "Pozycja Etykiety", + "kvLayout": "Układ", + "striped": "Paskowane rzędy", + "compact": "Tryb Kompaktowy", + "maxHeight": "Maksymalna Wysokość (px)", + "color": "Kolor", + "buttonLabel": "Etykieta Przycisku", + "buttonColor": "Kolor Przycisku", + "confirmText": "Tekst Potwierdzający", + "confirmTextHint": "Jeśli ustawione, okno dialogowe potwierdzenia pojawi się przed wykonaniem żądania", + "successMessage": "Wiadomość o powodzeniu", + "successMessageHint": "Wiadomość wyświetlana po pomyślnym wykonaniu", + "enabled": "Włączony", "template": { - "label": "", - "description": "" + "label": "Szablon JSX", + "description": "Szablon Mantine JSX. Uzyskaj dostęp do pól odpowiedzi API poprzez powiązanie danych (np. data.name). Używaj tylko kopmonentów na białej liście. Zabronione: constructor, __proto__, eval, Function, window, document." } }, "placeholder": { - "exampleLabel": "", - "exampleLabelShort": "", - "exampleHeader": "", - "exampleUnit": "", - "iconUrl": "", - "url": "", - "headerName": "", - "jsonPath": "", - "tablePath": "", - "columnJsonPath": "", - "valuePath": "", - "maxPath": "", - "goodValues": "", - "color": "" + "exampleLabel": "Temperatura CPU", + "exampleLabelShort": "Temp CPU", + "exampleHeader": "Nazwa", + "exampleUnit": "°C", + "iconUrl": "https://...", + "url": "https://api.example.com/api/v1/status", + "headerName": "X-API-Key", + "jsonPath": "$.data.value", + "tablePath": "$.data.items", + "columnJsonPath": "$.name", + "valuePath": "$.data.used", + "maxPath": "$.data.total", + "goodValues": "online, true, up", + "color": "niebieski" }, "cardStyleOption": { - "filled": "", - "outline": "", - "subtle": "" + "filled": "Wypełnione", + "outline": "Obrys", + "subtle": "Subtelny" }, "layoutOption": { - "list": "", - "grid": "" + "list": "Lista", + "grid": "Siatka" }, "sizeOption": { - "sm": "", - "md": "", - "lg": "", - "xl": "" + "sm": "Mały", + "md": "Średni", + "lg": "Duży", + "xl": "Bardzo duży" }, "labelPositionOption": { - "above": "", - "below": "" + "above": "Powyżej", + "below": "Poniżej" }, "fieldset": { - "general": "", - "connection": "", - "display": "" + "general": "Ogólne", + "connection": "Połączenie API", + "display": "Konfiguracja Wyświetlania" }, "displayTypeDescription": { - "singleValue": "", - "keyValue": "", - "table": "", - "statGrid": "", - "progressBars": "", - "statusIndicator": "", - "countGrid": "", - "raw": "", - "actionButton": "", - "customJsx": "" + "singleValue": "Pokaż jedną wyodrębnioną wartość z etykietą i jednostką", + "keyValue": "Pokaż wiele oznaczonych wartości obok siebie", + "table": "Pokaż wiersze danych w formacie tabeli", + "statGrid": "Kolorowe kart statystyk w siatce - takie jak widgety Pi-Hole lub Speedtest", + "progressBars": "Paski postępu pokazujące użycie lub ukończenie — dla dysku, pamięci, kwot", + "statusIndicator": "Kolorowe kropki stanu pokazujące stan online/offline lub stan zdrowia", + "countGrid": "Kompaktowe numer + licznik etykiety w ścisłej siatce", + "raw": "Sformatowany JSON do debugowania", + "actionButton": "Przycisk, który wywołuje zapytanie po kliknięciu", + "customJsx": "Własny układ JSX za pomocą komponentów Mantine z białej listy z pełnym powiązaniem danych API" }, "method": { - "GET": "", - "POST": "", - "PUT": "", - "DELETE": "", - "PATCH": "" + "GET": "GET", + "POST": "POST", + "PUT": "PUT", + "DELETE": "DELETE", + "PATCH": "PATCH" }, "authType": { - "none": "", - "bearer": "", - "basic": "", - "apiKeyHeader": "", - "apiKeyQuery": "" + "none": "None", + "bearer": "Bearer Token", + "basic": "Basic Auth", + "apiKeyHeader": "Klucz API (Nagłówek)", + "apiKeyQuery": "Klucz API (Query)" }, "displayType": { - "singleValue": "", - "keyValue": "", - "table": "", - "statGrid": "", - "progressBars": "", - "statusIndicator": "", - "countGrid": "", - "raw": "", - "actionButton": "", - "customJsx": "" + "singleValue": "Pojedyncza Wartość", + "keyValue": "Klucz-Wartość", + "table": "Tabela", + "statGrid": "Siatka statystyk", + "progressBars": "Paski Postępu", + "statusIndicator": "Wskaźnik statusu", + "countGrid": "Count Grid", + "raw": "Nieprzetworzony JSON", + "actionButton": "Przycisk Akcji", + "customJsx": "Niestandardowy JSX" }, "secret": { - "apiKey": "", - "username": "", - "password": "", - "savedPlaceholder": "" + "apiKey": "Klucz API / Token", + "username": "Nazwa użytkownika", + "password": "Hasło", + "savedPlaceholder": "•••••• (zapisane, pozostaw puste aby zachować)" }, "preview": { - "title": "", - "test": "", - "hint": "", - "rawResponse": "" + "title": "Podgląd", + "test": "Test", + "hint": "Kliknij test, aby wyświetlić podgląd odpowiedzi API w aktualnej konfiguracji.", + "rawResponse": "Nieprzetworzona odpowiedź JSON" }, "notification": { - "created": "", - "updated": "", - "deleted": "", - "imported": "", - "aiPromptCopied": "", - "aiPromptDescription": "", - "aiPromptCopy": "", - "aiPromptNoResponse": "", - "aiPromptCopyError": "", - "createError": "", - "updateError": "", - "importError": "", - "exportError": "", - "duplicated": "", - "duplicateError": "", - "toggleError": "", - "deleteError": "", - "previewError": "", - "executed": "", - "executeError": "" + "created": "Widget \"{name}\" został utworzony pomyślnie.", + "updated": "Widget \"{name}\" zaktualizowany pomyślnie.", + "deleted": "Widget \"{name}\" usunięty.", + "imported": "Widget został zaimportowany pomyślnie.", + "aiPromptCopied": "Prompt AI skopiowany do schowka", + "aiPromptDescription": "Skopiuj wstępnie zbudowany prompt, aby wkleić do dowolnego czatu AI (ChatGPT, Claude, Gemini...). Zawiera pełny schemat widgetu i odpowiedź API, aby AI mógł wygenerować gotową konfigurację JSON dla Ciebie.", + "aiPromptCopy": "Skopiuj do schowka", + "aiPromptNoResponse": "Wskazówka: Uruchom najpierw test, aby dołączyć odpowiedź API w prompcie w celu uzyskania lepszych wyników.", + "aiPromptCopyError": "Nie udało się skopiować prompta do schowka. Twoja przeglądarka może nie obsługiwać tej funkcji.", + "createError": "Nie udało się utworzyć widżetu. Sprawdź, czy endpoint API jest osiągalny.", + "updateError": "Nie udało się zaktualizować widgetu. Zmiany nie zostały zapisane.", + "importError": "Nie udało się zaimportować widgetu. Upewnij się, że plik jest prawidłowym eksportem niestandardowego widgetu JSON.", + "exportError": "Nie udało się wyeksportować widgetu.", + "duplicated": "Widget \"{name}\" zduplikowany.", + "duplicateError": "Nie udało się zduplikować widgetu.", + "toggleError": "Nie udało się przełączyć statusu widgetu.", + "deleteError": "Nie udało się usunąć widgetu.", + "previewError": "Nie udało się pobrać podglądu. Sprawdź URL i spróbuj ponownie.", + "executed": "Akcja wykonana pomyślnie.", + "executeError": "Akcja nie powiodła się: {error}" }, "table": { - "name": "", - "url": "", - "display": "" + "name": "Nazwa", + "url": "Adres URL", + "display": "Widok" }, "widget": { - "disabled": "" + "disabled": "Ten widget jest wyłączony" } }, "integration": { "page": { "list": { "title": "Integracje", - "search": "Wyszukaj integrację", + "search": "Szukaj integracji", "noResults": { "title": "Nie ma jeszcze integracji" } @@ -917,10 +917,10 @@ "label": "Wybierz istniejącą aplikację" }, "new": { - "title": "Nowy", + "title": "Nowa", "url": { "label": "Adres URL aplikacji", - "description": "Adres URL, który zostanie otwarty po kliknięciu aplikacji na pulpicie" + "description": "Adres URL, który zostanie otwarty po kliknięciu aplikacji na tablicy" } } } @@ -983,46 +983,47 @@ "placeholder": "Niestandardowy adres URL aplikacji" }, "apiKeySettings": { - "label": "" + "label": "Uzyskaj klucz API" } }, "action": { "create": "Nowa integracja" }, "grid": { - "widgetCount": "", - "noWidgets": "", - "detected": "" + "widgetCount": "{count} widgety", + "noWidgets": "Brak widgetów", + "detected": "Wykryto przez Docker" }, "category": { - "dnsHole": "", - "mediaService": "", - "calendar": "", - "mediaSearch": "", - "mediaRelease": "", - "mediaRequest": "", - "downloadClient": "", - "usenet": "", - "torrent": "", - "miscellaneous": "", - "smartHomeServer": "", - "indexerManager": "", - "healthMonitoring": "", - "search": "", - "mediaTranscoding": "", - "networkController": "", - "releasesProvider": "", - "notifications": "", - "firewall": "", - "timetable": "", - "photoService": "", - "documents": "", - "notes": "", - "mediaMonitoring": "", - "speedtest": "", - "analytics": "", - "mediaLibrary": "", - "uptimeMonitoring": "" + "dnsHole": "DNS", + "mediaService": "Media", + "calendar": "Kalendarz", + "mediaSearch": "Szukaj", + "mediaRelease": "Wydania", + "mediaRequest": "Zapytania", + "downloadClient": "Pobrania", + "usenet": "Usenet", + "torrent": "Torrent", + "miscellaneous": "Inne", + "smartHomeServer": "Smart Home", + "indexerManager": "Indexer", + "healthMonitoring": "Kondycja", + "search": "Szukaj", + "mediaTranscoding": "Transkodowanie", + "networkController": "Sieć", + "releasesProvider": "Wydania", + "notifications": "Powiadomienia", + "firewall": "Firewall", + "timetable": "Harmonogram", + "photoService": "Zdjęcia", + "documents": "Dokumenty", + "notes": "Notatki", + "mediaMonitoring": "Monitoring", + "speedtest": "Test prędkości", + "analytics": "Analityka", + "vpn": "VPN", + "mediaLibrary": "Biblioteka Multimediów", + "uptimeMonitoring": "Monitorowanie Czasu Pracy" }, "testConnection": { "action": { @@ -1215,26 +1216,26 @@ }, "invalidJson": { "title": "Nieprawidłowy JSON", - "message": "Odpowiedź nie była prawidłowym JSON'em" + "message": "Nie poprawny JSON w odpowiedzi" }, "wrongPath": { "title": "Błędna ścieżka", "message": "Ścieżka jest prawdopodobnie niepoprawna" }, "tooManyRequests": { - "title": "Zbyt wiele żądań w określonym czasie", - "message": "Wystąpiło zbyt wiele żądań. Prawdopodobnie jesteś ograniczony lub odrzucony przez system docelowy" + "title": "Zbyt wiele zapytań w określonym czasie", + "message": "Wystąpiło zbyt wiele zapytań. Prawdopodobnie jesteś ograniczony lub odrzucony przez system docelowy" } } }, "secrets": { "title": "Sekrety", - "lastUpdated": "Data ostatniej aktualizacji: {date}", + "lastUpdated": "Ostatnio zaktualizowane {date}", "notSet": { "label": "Nie ustawiono wartości", "tooltip": "Ten wymagany sekret nie został jeszcze ustawiony" }, - "secureNotice": "Ten sekret nie może zostać pobrany po utworzeniu", + "secureNotice": "Ten sekret nie może zostać odzyskany po utworzeniu", "reset": { "title": "Resetuj sekret", "message": "Czy na pewno chcesz zresetować ten sekret?" @@ -1277,8 +1278,8 @@ "newLabel": "Nowy adres URL" }, "opnsenseApiKey": { - "label": "Klucz API", - "newLabel": "Nowy klucz API" + "label": "Klucz API (Key)", + "newLabel": "Nowy klucz API (Key)" }, "opnsenseApiSecret": { "label": "Klucz API (Secret)", @@ -1297,8 +1298,8 @@ "newLabel": "Nowy klucz prywatny" }, "slug": { - "label": "", - "newLabel": "" + "label": "Slug strony statusu", + "newLabel": "Nowy Slug strony statusu" } } }, @@ -1308,33 +1309,33 @@ "full": "Pełny dostęp do integracji" }, "audiobookshelf": { - "name": "", - "description": "" + "name": "Audiobookshelf", + "description": "Samodzielnie hostowany serwer audiobooków i podcastów" }, "navidrome": { - "name": "", - "description": "" + "name": "Navidrome", + "description": "Samodzielnie hostowany serwer strumieniowania muzyki z kompatybilnością Subsonic API" }, "uptimeKuma": { - "name": "", - "description": "", + "name": "Uptime Kuma", + "description": "Samodzielnie hostowany serwer monitorujący czas pracy z publicznymi stronami statusu", "secret": { "slug": { - "description": "" + "description": "Slug twojej publicznej strony statusu. Domyślnie \"default\" jeśli nie ustawione." }, "apiKey": { - "description": "" + "description": "Opcjonalny klucz API dla uwierzytelnionego dostępu /metryki" } } }, "paperlessNgx": { - "name": "", - "description": "" + "name": "Paperless-ngx", + "description": "System zarządzania dokumentami do archiwizacji i organizacji zeskanowaych dokumnteów" } }, "media": { "plural": "Multimedia", - "search": "Znajdź media", + "search": "Znajdź multimedia", "field": { "name": "Nazwa", "size": "Rozmiar", @@ -1342,7 +1343,7 @@ }, "action": { "upload": { - "label": "Prześlij media", + "label": "Prześlij multimedia", "file": "Wybierz plik", "notification": { "success": { @@ -1354,14 +1355,14 @@ } }, "delete": { - "label": "Usuń media", - "description": "Czy na pewno chcesz usunąć media ?", + "label": "Usuń multimedia", + "description": "Czy na pewno chcesz usunąć multimedia ?", "notification": { "success": { "message": "Plik został usunięty pomyślnie" }, "error": { - "message": "Nie udało się usunąć mediów" + "message": "Nie udało się usunąć pliku" } } }, @@ -1369,11 +1370,11 @@ "label": "Kopiuj adres URL" }, "open": { - "label": "Otwórz media" + "label": "Otwórz multimedia" } }, "noResults": { - "title": "" + "title": "Brak przesłanych multimediów" } }, "common": { @@ -1384,7 +1385,7 @@ "add": "Dodaj", "apply": "Zastosuj", "backToOverview": "Powrót do widoku ogólnego", - "skip": "", + "skip": "Pomiń", "change": "Zmień", "create": "Utwórz", "createAnother": "Utwórz i zacznij od nowa", @@ -1406,24 +1407,27 @@ "checkLogs": "Sprawdź logi, aby uzyskać więcej informacji.", "tryAgain": "Spróbuj ponownie", "loading": "Ładowanie", - "show": "", - "hide": "" + "show": "Pokaż", + "hide": "Ukryj" }, "here": "tutaj", "iconPicker": { "label": "adres URL ikony", - "header": "Wpisz nazwę lub obiekty do filtrowania ikon... Homarr przeszuka dla Ciebie ikony {countIcons}." + "header": "Wpisz nazwę lub obiekty do filtrowania ikon... Homarr przeszuka dla Ciebie ikony {countIcons}.", + "headerLoading": "Wpisz, aby wyszukać ikony…", + "noResults": "Brak ikon pasujących do \"{search}\".", + "error": "Nie udało się załadować ikon." }, "colorScheme": { "options": { - "auto": "", + "auto": "Automatyczne", "light": "Jasny", "dark": "Ciemny" } }, "information": { "min": "Min", - "max": "Max.", + "max": "Maks", "days": "Dni", "hours": "Godziny", "minutes": "Minuty" @@ -1464,6 +1468,7 @@ "switchToDarkMode": "Przełącz na tryb nocny", "switchToLightMode": "Przełącz na tryb jasny", "management": "Zarządzanie", + "docker": "Docker", "preferences": "Twoje preferencje", "logout": "Wyloguj się", "login": "Zaloguj się", @@ -1501,8 +1506,8 @@ "number": "Wartość tego pola musi być mniejsza lub równa {maximum}" }, "custom": { - "passwordsDoNotMatch": "Wprowadzone hasła nie zgadzają się", - "passwordRequirements": "Twoje hasło nie spełnia wymagań.", + "passwordsDoNotMatch": "Hasła nie są takie same", + "passwordRequirements": "Twoje hasło nie spełnia wymagań", "boardAlreadyExists": "Tablica o tej nazwie już istnieje", "invalidFileType": "Nieprawidłowy typ pliku, oczekiwany {expected}", "invalidFileName": "Nieprawidłowa nazwa pliku", @@ -1573,7 +1578,7 @@ "label": { "create": "Nowa kategoria", "changePosition": "Zmiana pozycji", - "settings": "" + "settings": "Ustawienia" } }, "openAllInNewTabs": { @@ -1584,16 +1589,19 @@ }, "item": { "action": { - "create": "Nowa pozycja", - "import": "Importuj pozycję", - "edit": "Edytuj pozycję", - "moveResize": "Przenieś / zmień rozmiar pozycji", - "duplicate": "Duplikuj pozycję", - "remove": "Usuń pozycję" + "create": "Nowy element", + "import": "Importuj element", + "edit": "Edytuj element", + "moveResize": "Przenieś / zmień rozmiar element", + "duplicate": "Duplikuj element", + "remove": "Usuń element" }, "menu": { "label": { - "settings": "Ustawienia" + "settings": "Ustawienia", + "refresh": "Odśwież", + "options": "Opcje", + "actions": "Akcje" } }, "create": { @@ -1602,7 +1610,7 @@ "addToBoard": "Dodaj do tablicy" }, "moveResize": { - "title": "Przenieś / zmień rozmiar pozycji", + "title": "Przenieś / zmień rozmiar elementu", "field": { "width": { "label": "Szerokość" @@ -1619,17 +1627,17 @@ } }, "edit": { - "title": "Edytuj pozycje", + "title": "Edytuj element", "tab": { - "widget": "", - "app": "" + "widget": "Edytuj widget", + "app": "Edytuj aplikację" }, "app": { - "propagationNotice": "" + "propagationNotice": "Zmiany w tej aplikacji będą zastosowane wszędzie tam, gdzie jest używana spośród wszystkich Tablic." }, "advancedOptions": { "label": "Ustawienia zaawansowane", - "title": "Zaawansowane opcje pozycji" + "title": "Zaawansowane opcje elementu" }, "field": { "integrations": { @@ -1653,11 +1661,11 @@ }, "widget": { "integrationSelect": { - "configureIntegrationFirst": "", - "loading": "", - "placeholder": "", - "noResults": "", - "loadError": "" + "configureIntegrationFirst": "Najpierw połącz integrację", + "loading": "Ładowanie...", + "placeholder": "Wybierz opcję", + "noResults": "Nie znaleziono żadnych wyników", + "loadError": "Nie udało się załadować opcji" }, "app": { "name": "Aplikacja", @@ -1673,7 +1681,7 @@ "label": "Pokaż nazwę aplikacji" }, "pingEnabled": { - "label": "Włącz prosty ping" + "label": "Włącz sprawdzanie statusu" }, "layout": { "label": "Układ", @@ -1738,7 +1746,7 @@ "label": "Otwórz w nowej karcie" }, "withBorder": { - "label": "" + "label": "Pokaż obramowanie" }, "items": { "label": "Zakładki", @@ -1747,17 +1755,17 @@ } }, "archiveTeamWarrior": { - "name": "", - "description": "", + "name": "ArchiveTeam Warrior", + "description": "Monitor ArchiveTeam Warrior status, projekty i aktywne prace archwialne.", "option": { "showBroadcastMessage": { - "label": "" + "label": "Pokaż wiadomości nadawcze" } } }, "dnsHoleSummary": { - "name": "Podsumowanie Hole DNS", - "description": "Wyświetla podsumowanie Twojego hosta DNS", + "name": "Podsumowanie DNS Hole", + "description": "Wyświetla podsumowanie Twojego DNS Hole", "option": { "layout": { "label": "Układ", @@ -1778,7 +1786,7 @@ } }, "error": { - "internalServerError": "Nie udało się pobrać podsumowanie Hole DNS", + "internalServerError": "Nie udało się pobrać podsumowanie DNS Hole", "integrationsDisconnected": "Brak dostępnych danych, wszystkie integracje odłączone" }, "data": { @@ -1790,7 +1798,7 @@ "domainsTooltip": "Ze względu na wiele integracji, Homarr nie może obliczyć dokładnej liczby zablokowanych domen" }, "dnsHoleControls": { - "name": "Kontrola luk DNS", + "name": "Kontrola DNS Hole", "description": "Kontroluj PiHole lub AdGuard ze swojego pulpitu", "option": { "layout": { @@ -1890,11 +1898,14 @@ } }, "timetable": { - "name": "Rozkład", + "name": "Harmonogram", "description": "Wyświetla czas odjazdu dla stacji", "title": "Odjazdy {station}", "noStation": "Nie wybrano stacji", "option": { + "baseUrl": { + "label": "Szukaj adresu URL" + }, "station": { "label": "Stacja" } @@ -2083,34 +2094,34 @@ "label": "Zakres czasu", "option": { "1d": { - "label": "1 dzień" + "label": "1 Dzień" }, "5d": { - "label": "5 dni" + "label": "5 Dni" }, "1mo": { - "label": "1 miesiąc" + "label": "1 Miesiąc" }, "3mo": { - "label": "3 miesiące" + "label": "3 Miesiące" }, "6mo": { - "label": "6 miesięcy" + "label": "6 Miesięcy" }, "ytd": { "label": "Od początku roku" }, "1y": { - "label": "1 rok" + "label": "1 Rok" }, "2y": { - "label": "2 lata" + "label": "2 Lata" }, "5y": { - "label": "5 lat" + "label": "5 Lat" }, "10y": { - "label": "10 lat" + "label": "10 Lat" }, "max": { "label": "Maks" @@ -2118,31 +2129,31 @@ } }, "timeInterval": { - "label": "Interwał czasu", + "label": "Interwał Czasu", "option": { "5m": { - "label": "5 minut" + "label": "5 Minut" }, "15m": { - "label": "15 minut" + "label": "15 Minut" }, "30m": { - "label": "30 minut" + "label": "30 Minut" }, "1h": { - "label": "1 godzina" + "label": "1 Godzina" }, "1d": { - "label": "1 dzień" + "label": "1 Dzień" }, "5d": { - "label": "5 dni" + "label": "5 Dni" }, "1wk": { - "label": "1 tydzień" + "label": "1 Tydzień" }, "1mo": { - "label": "1 miesiąc" + "label": "1 Miesiąc" } } } @@ -2189,7 +2200,7 @@ "description": "Tylko dla bieżącej pogody" }, "useImperialSpeed": { - "label": "Prędkość wiatru w mph" + "label": "Użyj mph dla prędkości wiatru" }, "location": { "label": "Lokalizacja pogody" @@ -2198,7 +2209,7 @@ "label": "Pokaż miasto" }, "hasForecast": { - "label": "Pokaż Prognozę" + "label": "Pokaż prognozę" }, "forecastDayCount": { "label": "Liczba dni prognozy", @@ -2209,7 +2220,7 @@ "description": "Jak powinna wyglądać data" } }, - "currentWindSpeed": "{currentWindSpeed} km/h", + "currentWindSpeed": "{currentWindSpeed} {unit}", "dailyForecast": { "sunrise": "Wschód słońca", "sunset": "Zachód słońca", @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "Pokaż informacje o systemie plików" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Domyślna karta" }, @@ -2352,6 +2368,9 @@ "name": "Statystyki Dockera", "description": "Statystyki Twoich kontenerów (ten widżet można dodać tylko z uprawnieniami administratora)", "option": { + "columns": { + "label": "Kolumny do pokazania" + }, "enableRowSorting": { "label": "Włącz sortowanie elementów" }, @@ -2405,35 +2424,41 @@ "title": "Instancje" } }, + "vpn": { + "name": "VPN", + "description": "Przegląd Twoich połączeń VPN", + "serviceUnavailable": "VPN niedostępny", + "option": {} + }, "ups": { - "name": "", - "description": "", + "name": "UPS", + "description": "Monitoruj status urządzeń UPS przez serwer NUT", "option": { "showBattery": { - "label": "" + "label": "Pokaż baterię" }, "showLoad": { - "label": "" + "label": "Pokaż obciążenie" }, "showVoltage": { - "label": "" + "label": "Pokaż napięcie" } }, "status": { - "online": "", - "onBattery": "", - "lowBattery": "", - "charging": "", - "unknown": "" + "online": "Online", + "onBattery": "Na baterii", + "lowBattery": "Niski poziom baterii", + "charging": "Ładowanie", + "unknown": "Nieznany" }, "field": { - "runtime": "", - "load": "", - "input": "", - "output": "" + "runtime": "Czas pracy", + "load": "Załaduj", + "input": "Input", + "output": "Output" }, "error": { - "internalServerError": "" + "internalServerError": "Nie udało się pobrać danych z UPS" } }, "tracearr": { @@ -2492,101 +2517,101 @@ } }, "umami": { - "name": "", - "description": "", - "visitors": "", - "active": "", - "pageviews": "", - "visits": "", - "bounceRate": "", - "avgDuration": "", + "name": "Analityka Umami", + "description": "Wyświetl statystyki odwiedzających z instancji analityki Umami", + "visitors": "goście", + "active": "aktywny", + "pageviews": "wyświetlenia strony", + "visits": "odwiedziny", + "bounceRate": "bounce", + "avgDuration": "średnia", "option": { "websiteId": { - "label": "", - "description": "", - "placeholder": "", - "loading": "", - "noWebsites": "", - "configureIntegrationFirst": "" + "label": "Strona internetowa", + "description": "Wybierz stronę śledzoną przez instancję Umami", + "placeholder": "Wybierz stronę internetową", + "loading": "Ładowanie stron internetowych...", + "noWebsites": "Nie znaleziono żadnych stron internetowych", + "configureIntegrationFirst": "Połącz najpierw integrację Umami" }, "timeFrame": { - "label": "", - "description": "", + "label": "Przedział czasowy", + "description": "Okres czasu dla którego wyświetlać dane odwiedzających", "option": { - "today": "", - "24h": "", - "7d": "", - "30d": "", - "month": "", - "lastMonth": "" + "today": "Dzisiaj", + "24h": "Ostatnie 24 godziny", + "7d": "Ostatnie 7 dni", + "30d": "Ostatnie 30 dni", + "month": "W tym miesiącu", + "lastMonth": "Ostatni miesiąc" } }, "eventName": { - "label": "", - "description": "", - "placeholder": "", - "loading": "", - "noEvents": "", - "none": "", - "configureWebsiteFirst": "" + "label": "Zdarzenie", + "description": "Pokaż wszystkie wystąpienia określonego zdarzenia", + "placeholder": "Wybierz zdarzenie", + "loading": "Ładowanie zdarzeń...", + "noEvents": "Nie znaleziono zdarzeń", + "none": "Brak zdarzeń", + "configureWebsiteFirst": "Wybierz stronę aby załadować zdarzenia" }, "chartType": { - "label": "", - "description": "", + "label": "Typ wykresu", + "description": "Wyświetl styl wykresu", "option": { - "bar": "", - "sparkline": "" + "bar": "Wykres słupkowy", + "sparkline": "Sparkline" } }, "chartStyle": { - "label": "", - "description": "", + "label": "Styl wykresu", + "description": "Jak wyświetlić serię zdarzeń na wykresie", "option": { - "grouped": "", - "overlay": "" + "grouped": "Obok siebie", + "overlay": "Overlay (subset)" } }, "viewMode": { - "label": "", - "description": "", + "label": "Widok", + "description": "Co wyświetlić w zawartości widgetu", "option": { - "chart": "", - "events": "", - "topPages": "", - "topReferrers": "" + "chart": "Wykres", + "events": "Wykres zdarzeń", + "topPages": "Najlepsze strony", + "topReferrers": "Główne źródła" } }, "eventNames": { - "label": "", - "description": "", - "placeholder": "", - "loading": "", - "noEvents": "", - "configureWebsiteFirst": "" + "label": "Zdarzenia", + "description": "Wybierz jedno lub więcej zdarzeń do wykresu", + "placeholder": "Wybierz zdarzenie...", + "loading": "Ładowanie zdarzeń...", + "noEvents": "Nie znaleziono zdarzeń", + "configureWebsiteFirst": "Wybierz stronę aby załadować zdarzenia" }, "topCount": { - "label": "", - "description": "" + "label": "Elementy do wyświetlenia", + "description": "Liczba górnych elementów do wyświetlenia (maks. 500)" } }, "event": { - "occurrences": "", - "label": "" + "occurrences": "zdarzenia", + "label": "zdarzenia" }, "topPages": { - "direct": "" + "direct": "Bezpośrednio" }, "topReferrers": { - "direct": "" + "direct": "Bezpośrednio" }, "events": { - "selectPrompt": "" + "selectPrompt": "Wybierz zdarzenia do wykresu" }, "error": { - "noIntegration": "", - "noWebsite": "", - "noData": "", - "internalServerError": "" + "noIntegration": "Brak skonfigurowanej integracji Umami", + "noWebsite": "Brak skonfigurowanej strony internetowej", + "noData": "Brak dostępnych danych", + "internalServerError": "Nie udało się pobrać danych z Umami" } }, "common": { @@ -2685,48 +2710,48 @@ } }, "paperlessNgx": { - "name": "", - "description": "", - "documentsTotal": "", - "documentsInbox": "", - "correspondents": "", - "tags": "", - "documentTypes": "", - "inboxRatio": "", - "inbox": "", - "total": "", + "name": "Paperless-ngx", + "description": "Przegląd Twojego archiwum dokumentów Paperless-ngx", + "documentsTotal": "Łączna liczba dokumentów", + "documentsInbox": "Skrzynka odbiorcza", + "correspondents": "Korespondenci", + "tags": "Tagi", + "documentTypes": "Typy dokumentów", + "inboxRatio": "Współczynnik skrzynki odbiorczej", + "inbox": "skrzynka odbiorcza", + "total": "razem", "option": { "showInboxRatio": { - "label": "", - "description": "" + "label": "Pokaż współczynnik skrzynki odbiorczej", + "description": "Wyświetl stosunek liczby dokumentów z skrzynki do wszystkich w sekcji hero" }, "showInboxRing": { - "label": "", - "description": "" + "label": "Pokaż pierścień skrzynki odbiorczej", + "description": "Wyświetlaj pierścień postępu obok współczynkia skrzynki odbiorczej" }, "showDocumentsTotal": { - "label": "", - "description": "" + "label": "Pokaż wszystkie dokumenty", + "description": "Pokaż całkowitą liczbę dokumentów w archiwum" }, "showDocumentsInbox": { - "label": "", - "description": "" + "label": "Pokaż liczbę skrzynek odbiorczych", + "description": "Pokaż liczbę nieprzetworzonych dokumentów w skrzynce odbiorczej" }, "showCorrespondents": { - "label": "", - "description": "" + "label": "Pokaż korespondentów", + "description": "Pokaż całkowitą liczbę korespondentów" }, "showTags": { - "label": "", - "description": "" + "label": "Pokaż Tagi", + "description": "Pokaż całkowitą liczbę tagów" }, "showDocumentTypes": { - "label": "", - "description": "" + "label": "Pokaż typy dokumentów", + "description": "Pokaż całkowitą liczbę typów dokumentów" } }, "error": { - "internalServerError": "" + "internalServerError": "Nie udało się pobrać danych Paperless-ngx" } }, "immich-serverStats": { @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "Pokaż informacje o zdjęciu", "description": "Wyświetl datę zdjęcia oraz bieżący indeks na dole pokazu slajdów" + }, + "randomizePhotos": { + "label": "Losowe zdjęcia", + "description": "Pokaż zdjęcia w losowej kolejności, zamiast zawsze rozpoczynać się od tej samej kolejności albumu." } }, "description": "Pokaz slajdów zdjęć z Twojego albumu w Immich", @@ -3133,6 +3162,9 @@ "provider": { "label": "Dostawca" }, + "providerUrl": { + "label": "URL dostawcy" + }, "identifier": { "label": "Identyfikator", "placeholder": "Nazwa lub Właściciel/Nazwa" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "Brak dostawcy", "tooltip": "Nie udało się rozpoznać dostawcy. Ustaw go ręcznie po zaimportowaniu obrazów." + }, + "tokens": { + "label": "Tokeny dostawcy", + "token": "Token", + "configured": "Token skonfigurowany", + "notConfigured": "Brak ustawionych tokenów", + "save": "Zapisz" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "Nieprawidłowy identyfikator", "noMatchingVersion": "Nie znaleziono pasującej wersji", "noReleasesFound": "Nie znaleziono wydań!", - "noProviderSeleceted": "Nie wybrano dostawcy", - "noProviderResponse": "Brak odpowiedzi od dostawcy" + "noProviderSelected": "Nie wybrano dostawcy", + "noProviderResponse": "Brak odpowiedzi od dostawcy", + "unexpected": "Nieoczekiwany błąd dostawcy" } } }, @@ -3338,110 +3378,110 @@ } }, "uptimeKuma": { - "name": "", - "description": "", + "name": "Uptime Kuma", + "description": "Monitoruj statystyki czasu pracy ze strony statusu Uptime Kuma", "option": { "showAverageUptime": { - "label": "", - "description": "" + "label": "Pokaż średni czas pracy", + "description": "Wyświetlaj łączny średni 24-godzinny procent czasu pracy" }, "showUptimeRing": { - "label": "", - "description": "" + "label": "Pokaż pierścień czasu pracy", + "description": "Wyświetlaj pierścień postępu obok średniej wartości czasu pracy" }, "showTotalMonitors": { - "label": "", - "description": "" + "label": "Pokaż wszystkie monitory", + "description": "Pokaż ilośc wszystkich z konfigurowanych monitorów" }, "showUpCount": { - "label": "", - "description": "" + "label": "Pokaż ilość włączonych", + "description": "Pokaż ile monitorów obecnie jest włączonych" }, "showDownCount": { - "label": "", - "description": "" + "label": "Pokaż ilość wyłączonych", + "description": "Pokaż ile monitorów obecnie jest wyłączonych" }, "showPausedCount": { - "label": "", - "description": "" + "label": "Pokaż ilośc zatrzymanych", + "description": "Pokaż ile monitorów obecnie jest zatrzymane" } }, - "averageUptime": "", + "averageUptime": "Średni czas pracy (24 godz.)", "stats": { - "totalMonitors": "", - "upCount": "", - "downCount": "", - "pausedCount": "" + "totalMonitors": "Całkowita liczba monitorów", + "upCount": "Włączone", + "downCount": "Wyłączone", + "pausedCount": "Zatrzymane" }, "error": { - "internalServerError": "" + "internalServerError": "Nie udało się pobrać danych czasu pracy" } }, "audioStats": { - "name": "", - "description": "", + "name": "Audio stats", + "description": "Statystyki biblioteki i odtwarzania z Navidrome lub Audiobookshelf", "option": { "showArtists": { - "label": "", - "description": "" + "label": "Pokaż artystów", + "description": "Pokaż całkowitą liczbę artystów w twojej bibliotece muzycznej (Navidrome)" }, "showAlbums": { - "label": "", - "description": "" + "label": "Pokaż albumy", + "description": "Pokaż całkowitą liczbę albumów w twojej bibliotece muzycznej (Navidrome)" }, "showSongs": { - "label": "", - "description": "" + "label": "Pokaż utwory", + "description": "Pokaż całkowitą liczbę utworów w twojej bibliotece muzycznej (Navidrome)" }, "showNowPlaying": { - "label": "", - "description": "" + "label": "Pokaż teraz odtwarzane", + "description": "Pokaż aktualnie odtwarzane utwory (Navidrome)" }, "showNowPlayingList": { - "label": "", - "description": "" + "label": "Pokaż listę teraz odtwarzanych", + "description": "Pokaż poszczególne utwory zamiast tylko aktywnej liczby strumieni (Navidrome)" }, "maxNowPlayingItems": { - "label": "", - "description": "" + "label": "Maksymalna ilość teraz odtwarzanych elementów", + "description": "Maksymalna liczba elementów wyświetlanych na liście teraz odtwarzanych (Navidrome)" }, "showLibraryCount": { - "label": "", - "description": "" + "label": "Pokaż ilość bibliotek", + "description": "Pokaż liczbę bibliotek na twojej instancji (Audiobookshelf)" }, "showAudiobooks": { - "label": "", - "description": "" + "label": "Pokaż audiobooki", + "description": "Pokaż całkowitą liczbę audiobooków we wszystkich bibliotekach (Audiobookshelf)" }, "showPodcasts": { - "label": "", - "description": "" + "label": "Pokaż podcasty", + "description": "Pokaż całkowitą liczbę podcastów we wszystkich bibliotekach (Audiobookshelf)" }, "showListeningTime": { - "label": "", - "description": "" + "label": "Pokaż czas słuchania", + "description": "Pokaż całkowity czas słuchania (Audiobookshelf)" }, "showActiveSessions": { - "label": "", - "description": "" + "label": "Pokaż aktywne sesje", + "description": "Pokaż liczbę aktualnie aktywnych sesji odtwarzania (Audiobookshelf)" }, "compactMode": { - "label": "", - "description": "" + "label": "Tryb kompaktowy", + "description": "Użyj gęstszego układu z mniejszymi płytkami, idealnego dla widgetów małych rozmiarów" } }, - "artists": "", - "albums": "", - "songs": "", - "libraryCount": "", - "audiobooks": "", - "podcasts": "", - "listeningTime": "", - "activeSessions": "", - "nowPlaying": "", - "nowPlayingCount": "", + "artists": "Artyści", + "albums": "Albumy", + "songs": "Utwory", + "libraryCount": "Biblioteki", + "audiobooks": "Audiobooki", + "podcasts": "Podcasty", + "listeningTime": "Czas Słuchania", + "activeSessions": "Aktywne Sesje", + "nowPlaying": "Teraz Odtwarzane", + "nowPlayingCount": "{count, plural, one {# stream} other {# streams}}", "error": { - "internalServerError": "" + "internalServerError": "Nie udało się pobrać danych audio stats" } }, "systemResources": { @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "Pokaż pasek wykorzystania w tle" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3510,254 +3555,260 @@ } }, "customApi": { - "name": "", - "description": "", + "name": "Niestandardowe API", + "description": "Wyświetlaj dane z dowolnego zewnętrznego API z niestandardową konfiguracją.", "option": { "definitionId": { - "label": "" + "label": "Definition ID" }, "refreshInterval": { - "label": "", - "description": "" + "label": "Interwał odświeżania", + "description": "Jak często pobierać dane z API (w sekundach)" } }, - "noDefinition": "", - "definitionNotFound": "", - "noDisplayOutput": "", - "executeConfirm": "", - "executing": "", - "executeSuccess": "", - "executeFailed": "" + "noDefinition": "Nie wybrano definicji", + "definitionNotFound": "Nie znaleziono definicji widgetu. Być może została usunięta.", + "noDisplayOutput": "No display output", + "executeConfirm": "Czy jesteś pewien, że chcesz wykonać to zadanie?", + "executing": "Wykonywanie...", + "executeSuccess": "Akcja wykonana pomyślnie", + "executeFailed": "Akcja nie powiodła się. Sprawdź konfigurację widgetu." }, "beszelSystemTable": { - "name": "", - "description": "", + "name": "Systemy Beszel (Tabela)", + "description": "Widok tabeli wszystkich systemów monitorowanych przez Beszel z sortowalnymi kolumnami i wskaźnikami statusu", "option": { "statusFilter": { - "label": "" + "label": "Filtry statusu" }, "sortBy": { - "label": "" + "label": "Sortuj według" }, "sortDirection": { - "label": "" + "label": "Kierunek sortowania" }, "showCpu": { - "label": "" + "label": "Pokaż CPU" }, "showMemory": { - "label": "" + "label": "Pokaż pamięć" }, "showDisk": { - "label": "" + "label": "Pokaż dysk" }, "showGpu": { - "label": "" + "label": "Pokaż GPU" }, "showLoadAvg": { - "label": "" + "label": "Pokaż średnie obciążenie" }, "showNet": { - "label": "" + "label": "Pokaż sieć" }, "showTemp": { - "label": "" + "label": "Pokaż temperaturę" }, "showBattery": { - "label": "" + "label": "Pokaż baterię" }, "showServices": { - "label": "" + "label": "Pokaż usługi" }, "showUptime": { - "label": "" + "label": "Pokaż czas pracy" }, "showAgent": { - "label": "" + "label": "Pokaż wersję agenta" } }, - "noRecords": "", + "noRecords": "Nie znaleziono systemów", "error": { - "internalServerError": "" + "internalServerError": "Nie można połączyć się z Beszel. Sprawdź ustawienia integracji." }, "column": { - "system": "", - "cpu": "", - "memory": "", - "disk": "", - "gpu": "", - "loadAvg": "", - "net": "", - "temp": "", - "battery": "", - "services": "", - "uptime": "", - "agent": "" + "system": "System", + "cpu": "CPU", + "memory": "Pamięć", + "disk": "Dysk", + "gpu": "GPU", + "loadAvg": "Śr. Obciążenie", + "net": "Sieć", + "temp": "Temp.", + "battery": "Bateria", + "services": "Usługi", + "uptime": "Czas pracy", + "agent": "Agent" } }, "beszelSystemGrid": { - "name": "", - "description": "", + "name": "Beszel Systems (Siatka)", + "description": "Widok siatki wszystkich systemów monitorowanych przez Beszel z miernikami w czasie rzeczywistym", "option": { "statusFilter": { - "label": "" + "label": "Filtr statusu" }, "showCpu": { - "label": "" + "label": "Pokaż CPU" }, "showMemory": { - "label": "" + "label": "Pokaż pamięć" }, "showDisk": { - "label": "" + "label": "Pokaż dysk" }, "showGpu": { - "label": "" + "label": "Pokaż GPU" }, "showLoadAvg": { - "label": "" + "label": "Pokaż średnie obciążenie" }, "showNet": { - "label": "" + "label": "Pokaż sieć" }, "showTemp": { - "label": "" + "label": "Pokaż temperaturę" }, "showBattery": { - "label": "" + "label": "Pokaż baterię" }, "showServices": { - "label": "" + "label": "Pokaż usługi" }, "showUptime": { - "label": "" + "label": "Pokaż czas pracy" }, "showAgent": { - "label": "" + "label": "Pokaż wersję agenta" } }, "error": { - "internalServerError": "" + "internalServerError": "Nie można połączyć się z Beszel. Sprawdź ustawienia integracji." + }, + "empty": { + "noSystems": "Nie znaleziono systemów" }, "metric": { - "cpu": "", - "memory": "", - "disk": "", - "gpu": "", - "loadAvg": "", - "net": "", - "temp": "", - "battery": "", - "services": "", - "uptime": "", - "agent": "" + "cpu": "CPU", + "memory": "Pamięć", + "disk": "Dysk", + "gpu": "GPU", + "loadAvg": "Śr. Obciążenie", + "net": "Sieć", + "temp": "Temp.", + "battery": "Bateria", + "services": "Usługi", + "uptime": "Czas pracy", + "agent": "Agent" } }, "beszelAlerts": { - "name": "", - "description": "", + "name": "Alerty Beszel", + "description": "Zobacz konfiguracje i historię alarmów Beszel", "option": { "showHistory": { - "label": "" + "label": "Pokaż historię alertów" }, "maxHistoryItems": { - "label": "" + "label": "Maksymalna ilość elementów historii" } }, - "empty": "", + "empty": "Brak skonfigurowanych alertów", "error": { - "internalServerError": "" + "internalServerError": "Nie można połączyć się z Beszel. Sprawdź ustawienia integracji." }, - "history": "", - "resolved": "", + "history": "Historia", + "resolved": "Rozwiązane", "status": { - "triggered": "", - "ok": "" + "triggered": "Wywołane", + "ok": "OK" } }, "beszelSystemStats": { - "name": "", - "description": "", + "name": "Beszel System Stats", + "description": "Wykresy okresów czasowych dla CPU, pamięci, dysku, sieci i metryki kontenerów Docker", "option": { "systemId": { - "label": "", - "description": "" + "label": "System", + "description": "Wybierz system Beszel do wyświetlania statystyk dla" }, "timePeriod": { - "label": "" + "label": "Okres czasu" }, "showCpu": { - "label": "" + "label": "Pokaż wykres CPU" }, "showMemory": { - "label": "" + "label": "Pokaż wykres pamięci" }, "showDisk": { - "label": "" + "label": "Pokaż wykres dysku" }, "showDiskIO": { - "label": "" + "label": "Pokaż wykres I/O dysku" }, "showNetwork": { - "label": "" + "label": "Pokaż wykres sieci" }, "showDockerCpu": { - "label": "" + "label": "Pokaż wykres CPU Dockera" }, "showDockerMemory": { - "label": "" + "label": "Pokaż wykres Pamięci Dockera" }, "showDockerNetwork": { - "label": "" + "label": "Pokaż wykres Sieci Dockera" } }, - "selectSystem": "", + "selectSystem": "Wybierz system", + "empty": { + "noSystems": "Nie znaleziono systemów" + }, "error": { - "internalServerError": "", - "liveConnectionFailed": "", - "switchToHistorical": "", - "systemNotFound": "" + "internalServerError": "Nie można połączyć się z Beszel. Sprawdź ustawienia integracji.", + "liveConnectionFailed": "Połączenie na żywo nie powiodło się. Twoja instancja Beszel może nie obsługiwać strumieniowania w czasie rzeczywistym.", + "switchToHistorical": "Przełącz na widok historyczny", + "systemNotFound": "Nie znaleziono wybranego systemu. Wybierz inny system." }, "chart": { "cpu": { - "title": "", - "subtitle": "", - "series": "" + "title": "Użycie CPU", + "subtitle": "Średnie wykorzystanie procesora w całym systemie", + "series": "CPU" }, "memory": { - "title": "", - "subtitle": "", - "series": "", - "cache": "" + "title": "Użycie Pamięci", + "subtitle": "Dokładne wykorzystanie w odnotowanym czasie", + "series": "W Użyciu", + "cache": "Cache/Buffers" }, "disk": { - "title": "", - "subtitle": "", - "series": "" + "title": "Użycie Dysku", + "subtitle": "Użycie partycji root", + "series": "W Użyciu" }, "diskIO": { - "title": "", - "subtitle": "", - "read": "", - "write": "" + "title": "I/O Dysku", + "subtitle": "Przepustowość głównego systemu plików", + "read": "Odczyt", + "write": "Zapis" }, "network": { - "title": "", - "subtitle": "", - "sent": "", - "recv": "" + "title": "Przepustowość", + "subtitle": "Ruch sieciowy interfejsów publicznych", + "sent": "Wysłane", + "recv": "Odebrane" }, "dockerCpu": { - "title": "", - "subtitle": "" + "title": "Użycie CPU Docker", + "subtitle": "Średnie wykorzystanie procesora kontenerów" }, "dockerMemory": { - "title": "", - "subtitle": "" + "title": "Wykorzystanie pamięci przez Docker", + "subtitle": "Użycie pamięci przez kontenery Docker" }, "dockerNetwork": { - "title": "", - "subtitle": "" + "title": "I/O Sieci Dockera", + "subtitle": "Ruch sieciowy kontenerów Docker" } } } @@ -4133,13 +4184,18 @@ "title": "Nie znaleziono", "text": "Nie znaleziono żądanego zasobu." }, + "error": { + "title": "Nie można załadować tej strony", + "text": "Wystąpił nieoczekiwany błąd podczas ładowania tej strony. Twoja nawigacja jest nadal dostępna — spróbuj załadować ponownie lub użyj innej sekcji z paska bocznego.", + "digest": "Odniesienie błędu: {digest}" + }, "navbar": { "items": { "home": "Strona główna", "boards": "Tablice", "apps": "Aplikacje", "integrations": "Integracje", - "customWidgets": "", + "customWidgets": "Niestandardowe Widgety", "searchEngies": "Wyszukiwarki", "medias": "Media", "users": { @@ -4159,7 +4215,7 @@ "api": "API", "certificates": "Certyfikaty", "tasks": "Zadania", - "backup": "" + "backup": "Kopia Zapasowa" } }, "settings": "Ustawienia", @@ -4240,7 +4296,7 @@ "private": "Ta tablica jest prywatna" }, "noResults": { - "title": "" + "title": "Brak tablic" }, "modal": { "createBoard": { @@ -4274,20 +4330,20 @@ "firstDayOfWeek": "Pierwszy dzień tygodnia", "accessibility": "Ułatwienia dostępu", "onboardingTours": { - "title": "", - "description": "", - "reset": "", + "title": "Samouczek", + "description": "Zresetuj samouczki, aby zobaczyć je ponownie podczas następnej wizyty w obszarze zarządzania lub na tablicy.", + "reset": "Zresetuj samouczki", "resetSuccess": { - "title": "", - "message": "" + "title": "Zresetuj samouczki", + "message": "Samouczki pojawią się ponownie podczas następnej wizyty" }, "status": { - "completed": "", - "notStarted": "" + "completed": "Ukończony", + "notStarted": "Nie rozpoczęty" }, "name": { - "management": "", - "dashboard": "" + "management": "Zarządzanie", + "dashboard": "Tablica" } } } @@ -4475,7 +4531,7 @@ "defaultColorScheme": { "label": "Domyślny schemat kolorów", "options": { - "auto": "", + "auto": "Auto", "light": "Jasny", "dark": "Ciemny" } @@ -4499,86 +4555,14 @@ "disabled": "Wyłączone" }, "job": { - "minecraftServerStatus": { - "label": "Status serwera Minecraft" - }, - "timetable": { - "label": "Harmonogram" - }, "iconsUpdater": { "label": "Aktualizator ikon" }, "analytics": { "label": "Analityka" }, - "smartHomeEntityState": { - "label": "Stan jednostki Smart Home" - }, "ping": { "label": "Pingi" - }, - "mediaServer": { - "label": "Serwery mediów" - }, - "mediaOrganizer": { - "label": "Organizatory mediów" - }, - "downloads": { - "label": "Pobrane " - }, - "mediaRequestStats": { - "label": "Statystyki żądań mediów" - }, - "mediaRequestList": { - "label": "Lista żądań mediów" - }, - "rssFeeds": { - "label": "Kanały RSS" - }, - "indexerManager": { - "label": "Menedżer indeksowania" - }, - "healthMonitoring": { - "label": "Monitorowanie zdrowia systemu" - }, - "dnsHole": { - "label": "Dane DNS Hole" - }, - "updateChecker": { - "label": "Sprawdzanie aktualizacji" - }, - "mediaTranscoding": { - "label": "Transkodowanie mediów" - }, - "networkController": { - "label": "Kontroler sieci" - }, - "refreshNotifications": { - "label": "Aktualizacje powiadomień" - }, - "dockerContainers": { - "label": "Kontenery Dockera" - }, - "firewallCpu": { - "label": "Procesor firewalla" - }, - "firewallMemory": { - "label": "Pamięć firewalla" - }, - "firewallVersion": { - "label": "Wersja firewalla" - }, - "firewallInterfaces": { - "label": "Interfejsy firewalla" - }, - "weather": { - "label": "Pogoda" - }, - "tracearr": { - "label": "Tracearr" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -4687,213 +4671,213 @@ } }, "mcp": { - "label": "", - "title": "", - "subtitle": "", - "description": "", - "noApiKey": "", - "createApiKeyButton": "", + "label": "AI / MCP", + "title": "Integracja AI", + "subtitle": "Model Context Protocol (MCP) Server", + "description": "Połącz asystentów AI do twojej instancji Homarr i kontroluj cały homelab poprzez naturalny język. Szukaj i proś o multimedia na Overseerr, zarządzaj konterami Docker, włączaj/wyłączaj Pi-hole, sprawdź status zdrowia, kontroluj urządzenia Home Assistant i więcej - wszystko to przez asystenta AI.", + "noApiKey": "Potrzebujesz klucza API do uwierzytelnienia z serwerem MCP.", + "createApiKeyButton": "Utwórz klucz API", "endpoint": { - "title": "" + "title": "MCP Endpoint" }, "authentication": { - "title": "", - "intro": "", + "title": "Authentication", + "intro": "Możesz uwierzytelnić się z serwerem MCP przy użyciu klucza API lub OAuth. Obie metody zapewniają taki sam poziom dostępu.", "apiKey": { - "title": "", - "description": "" + "title": "API Key (manual)", + "description": "W nagłówku ApiKey przekaż pełny klucz API w konfiguracji klienta MCP." }, "oauth": { - "title": "", - "description": "", - "hint": "" + "title": "OAuth (browser login)", + "description": "Klienci obsługujący OAuth (tacy jak Claude Code, opencode lub Cursor) mogą uwierzytelniać się poprzez otwarcie okna przeglądarki. Uruchom polecenie authi w swoim kliencie, zaloguj się do Homarr w przeglądarce i jesteś połączony — klucz API jest tworzony automatycznie.", + "hint": "Tokeny OAuth pokazują się jako zwykłe klucze API w zakładce Uwierzytelniania. Możesz je cofnąć w każdej chwili." }, - "permissions": "" + "permissions": "Klucz API określa uprawnienia dostępne dla AI — działa z takim samym poziomem dostępu, jak właściciel klucza." }, "clientConfig": { - "title": "", + "title": "Konfiguracja klienta", "streamableHttp": { - "recommended": "", - "title": "", - "worksWith": "", - "addToConfig": "", - "cursor": "", - "claudeDesktop": "", - "claudeCode": "", - "vsCodeCopilot": "", - "replaceHint": "" + "recommended": "Rekomendowane", + "title": "Streamowalny HTTP", + "worksWith": "Działą z Cursor, Claude Desktop, VS Code Copliot Windsurf, Claude Code", + "addToConfig": "Dodaj do pliku konfiguracji klienta MCP:", + "cursor": "{name}: {file} w twoim projekcie lub ustawieniach globalnych", + "claudeDesktop": "{name}: {file}", + "claudeCode": "{name}: {file} lub użyj {command}", + "vsCodeCopilot": "{name}: {file} w ustawieniach obszaru roboczego", + "replaceHint": "Zastąp zastępczy symbol kluczem API, który otrzymałeś podczas jego tworzenia." }, "stdio": { - "title": "", - "description": "" + "title": "stdio (starsze klienty przez mpc-remote bridge)", + "description": "Dla klientów, którzy obsługują tylko transport stdio. Wymaga zainstalowania lokalnie {dependency} (Node.js):" } }, "testConnection": { - "title": "", - "description": "", - "hint": "" + "title": "Przetestuj Swoje Połączenie", + "description": "Uruchom to w swoim terminalu, aby sprawdzić, czy endpoint jest osiągalny, a Twój klucz API działa:", + "hint": "Pomyślna odpowiedź zwraca funkcje serwera MCP. Jeżeli widzisz błąd, sprawdź odpowiedź JSON po wskazówki." }, "availableTools": { - "title": "", - "description": "", - "columnType": "", - "columnName": "", - "columnDescription": "" + "title": "Dostępne Narzędzia", + "description": "Dostępne narzędzia: {count}. Rozszerz kategorię, aby zobaczyć poszczególne narzędzia i ich opisy.", + "columnType": "Typ", + "columnName": "Narzędzie", + "columnDescription": "Opis" }, "troubleshooting": { - "title": "", + "title": "Rozwiązywanie problemów", "missingHeader": { - "title": "", - "description": "" + "title": "\"Missing ApiKey header\"", + "description": "Twój klient nie wysyła nagłówka {header}. Zweryfikuj czy sekcja {section} w konfiguracji MCP jest poprawna. Niektórzy klienci wymagają ponownego uruchomienia po zmianie konfiguracji." }, "invalidFormat": { - "title": "", - "description": "" + "title": "\"Invalid ApiKey format\"", + "description": "Upewnij się, że skopiowałeś pełny klucz API, a nie tylko jego część." }, "invalidKey": { - "title": "", - "description": "" + "title": "\"Invalid API key\"", + "description": "Identyfikator klucza nie został znaleziony lub token jest nieprawidłowy. Klucze API można wyświetlić tylko raz po utworzeniu. Jeśli go zgubiłeś, usuń stary klucz i utwórz nowy." }, "connectionRefused": { - "title": "", - "description": "" + "title": "Odmowa połączenia / przekroczenie czasu połączenia", + "description": "Upewnij się, że adres URL jest dostępny od klienta AI. Jeśli Homarr działa za odwróconym proxy lub w Docker, użyj zewnętrznego adresu URL (nie {localhost}). Sprawdź, czy port jest poprawny (domyślnie: 7575 dla Docker)." }, "toolsNotAppearing": { - "title": "", - "description": "" + "title": "Narzędzia nie pojawiają się", + "description": "Niektóre narzędzia wymagają konfiguracji specyficznych integracji (np. Docker, Overseerr). Lista narzędzi zależy od tego, co jest dostępne w twojej instancji Homarr." } }, - "copy": "", + "copy": "Kopiuj", "oneClick": { - "title": "", - "subtitle": "", - "vsCode": "", - "cursor": "", - "copyUrl": "", - "copied": "", - "copyHint": "", - "manualConfig": "" + "title": "Zainstaluj jednym kliknięciem", + "subtitle": "Używa OAuth - uwierzytelniaj w przeglądarce przy pierwszym użyciu", + "vsCode": "Zainstaluj w VS Code", + "cursor": "Zainstaluj w Cursor", + "copyUrl": "Kopiuj URL MCP", + "copied": "Skopiowano!", + "copyHint": "Dla ChatGPT, Claude Code, lub dowolnego klienta MCP", + "manualConfig": "Konfiguracja ręczna" } } } }, "backup": { - "title": "", - "metaTitle": "", + "title": "Pełna kopia bazy danych", + "metaTitle": "Kopia Zapasowa i Przywracanie", "export": { - "title": "", - "description": "", - "button": "", - "error": "", + "title": "Eksportuj Bazę Danych", + "description": "Pobierz pełną kopię zapasową bazy danych SQLite zawierającą wszystkie ustawienia, użytkowników, tablice i szyfrowane sekrety integracyji. Kopia zapasowa jest powiązana jako plik ZIP z kluczem szyfrowania i metadanymi.", + "button": "Pobierz Kopię Zapasową", + "error": "Eksportowanie nie powiodło się", "confirm": { - "title": "", - "warningTitle": "", - "warningBody": "", - "typePrompt": "", - "submit": "", - "cancel": "" + "title": "Eksport danych wrażliwych", + "warningTitle": "Ten eksport zawiera całą Twoją bazę danych.", + "warningBody": "Kopia zapasowa obejmuje wszystkich użytkowników, sekrety integracji (zaszyfrowane), klucze API, tablice i dane aplikacji. Przechowuj wyeksportowany plik bezpiecznie i nie udostępniaj go.", + "typePrompt": "Wpisz \"{phrase}\", aby kontynuować", + "submit": "Eksportuj Bazę Danych", + "cancel": "Anuluj" } }, "import": { - "title": "", - "description": "", - "warning": "", + "title": "Przywróć Bazę Danych", + "description": "Prześlij poprzednio wyeksportowaną kopię zapasową ZIP, aby przywrócić bazę danych. To zastąpi całą bazę danych. Jeśli kopia zapasowa została utworzona przy użyciu innego klucza szyfrowania, sekrety zostaną automatycznie ponownie zaszyfrowane.", + "warning": "Przywracanie kopii zapasowej całkowicie zastąpi Twoją aktualną bazę danych i zrestartuje aplikację. Tej akcji nie można cofnąć.", "dropzone": { - "title": "", - "description": "" + "title": "Upuść kopię zapasową ZIP tutaj lub kliknij aby przeglądać", + "description": "Akceptowane są tylko pliki .zip z kopii zapasowej Homarr SQLite" }, - "changeFile": "", - "button": "", + "changeFile": "Zmień plik", + "button": "Przywróć Bazę Danych", "success": { - "title": "", - "message": "" + "title": "Przywracanie Ukończone", + "message": "Baza danych została pomyślnie przywrócona. Aplikacja uruchamia się ponownie..." }, - "error": "" + "error": "Importowanie nie powiodło się" }, "preview": { - "title": "", - "exported": "", - "loading": "", - "errorTitle": "", - "entity": "", - "count": "", - "boards": "", + "title": "Zawartość kopii zapasowej", + "exported": "Wyeksportowano {date}", + "loading": "Analizowanie pliku kopii zapasowej...", + "errorTitle": "Podgląd Błędu", + "entity": "Encja", + "count": "Ilość", + "boards": "Tablice", "entities": { - "board": "", - "user": "", - "app": "", - "integration": "", - "item": "", - "media": "", - "search_engine": "" + "board": "Tablice", + "user": "Użytkownicy", + "app": "Aplikacje", + "integration": "Integracje", + "item": "Widgety", + "media": "Multimedia", + "search_engine": "Wyszukiwarki" }, - "analyzeError": "", - "invalidMissingMetadata": "", - "invalidMissingDb": "" + "analyzeError": "Nie udało się przeanalizować pliku kopii zapasowej", + "invalidMissingMetadata": "Nieprawidłowa kopia zapasowa: brakuje metadata.json", + "invalidMissingDb": "Nieprawidłowa kopia zapasowa: brakuje db.sqlite" }, "restore": { "dropzone": { - "title": "", - "description": "" + "title": "Upuść tutaj kopię zapasową ZIP", + "description": "Akceptuje pliki .zip z kopii zapasowej Homarr SQLite (maksymalnie 256 MB)" }, - "analyzing": "", - "changeFile": "", - "continueToRestore": "", - "tryAgain": "", - "analysisError": "", + "analyzing": "Analizowanie kopii zapasowej...", + "changeFile": "Zmień plik", + "continueToRestore": "Kontynuuj przywracanie", + "tryAgain": "Spróbuj ponownie", + "analysisError": "Błąd analizy", "preview": { - "title": "", - "exported": "", - "entity": "", - "count": "", - "boards": "", + "title": "Zawartość kopii zapasowej", + "exported": "Eksportowano {date}", + "entity": "Encja", + "count": "Ilość", + "boards": "Tablice", "entities": { - "board": "", - "user": "", - "app": "", - "integration": "", - "item": "", - "media": "", - "search_engine": "" + "board": "Tablice", + "user": "Użytkownicy", + "app": "Aplikacje", + "integration": "Integracje", + "item": "Widgety", + "media": "Multimedia", + "search_engine": "Wyszukiwarki" }, - "migrationsApplied": "", - "migrationsAppliedPlural": "", - "migrationsOlderVersion": "", - "migrationsOlderVersionPlural": "", - "migrationsUpToDate": "", - "upToDate": "", - "migrationsPending": "" + "migrationsApplied": "{count} migracja została zastosowana", + "migrationsAppliedPlural": "{count} migracje zostały zastosowane", + "migrationsOlderVersion": "Ta kopia zapasowa została wykonana ze starszej wersji Homarr. {count} migracja schematu została zastosowana, aby ją zaktualizować.", + "migrationsOlderVersionPlural": "Ta kopia zapasowa została wykonana ze starszej wersji Homarr. {count} migracje schematu zostały zastosowane, aby ją zaktualizować.", + "migrationsUpToDate": "Schemat bazy danych jest aktualny (zastosowano {count} migracji)", + "upToDate": "Na bieżąco", + "migrationsPending": "{count} oczekujące" }, "migration": { - "title": "", - "migrationLabel": "" + "title": "Stosowanie migracji ({current}/{total})", + "migrationLabel": "Migracja {index}" }, "confirm": { - "title": "", - "warningTitle": "", - "warningBody": "", - "typePrompt": "", - "cancel": "", - "submit": "" + "title": "Czynność niszczycielska", + "warningTitle": "To nieodwracalnie zastąpi całą Twoją bazę danych.", + "warningBody": "Wszystkie bieżące tablice, użytkownicy, aplikacje, integracje, ustawienia i sesje zostaną utracone. Tej czynności nie można cofnąć. Aplikacja zostanie uruchomiona ponownie po przywróceniu.", + "typePrompt": "Wpisz \"{phrase}\", aby kontynuować", + "cancel": "Anuluj", + "submit": "Przywrócono Bazę Danych" }, "progress": { - "title": "", - "extracting": "", - "migrating": "", - "encrypting": "", - "swapping": "", - "restarting": "" + "title": "Przywracanie bazy danych...", + "extracting": "Rozpakowywanie archiwum kopii zapasowej", + "migrating": "Wdrażanie migracji bazy danych", + "encrypting": "Ponowne szyfrowanie sekretów integracji", + "swapping": "Zastępowanie pliku bazy danych", + "restarting": "Ponowne uruchamianie serwera" }, "complete": { - "title": "", - "message": "" + "title": "Przywrócono kopię zapasową", + "message": "Baza danych została pomyślnie przywrócona. Serwer uruchamia się ponownie — odśwież stronę za kilka sekund." }, "failed": { - "title": "" + "title": "Przywracanie nie powiodło się" }, - "invalidMissingMetadata": "", - "invalidMissingDb": "", - "analyzeError": "" + "invalidMissingMetadata": "Nieprawidłowa kopia zapasowa: brakuje metadata.json", + "invalidMissingDb": "Nieprawidłowa kopia zapasowa: brakuje db.sqlite", + "analyzeError": "Nie udało się przeanalizować kopii zapasowej" } } }, @@ -4943,8 +4927,8 @@ "search": "Szukaj wśród {count} kontenerów", "selected": "Wybrano {selectCount} z {totalCount} kontenerów", "footer": "Łącznie {count} kontenerów", - "totalCpu": "", - "totalMemory": "" + "totalCpu": "Całkowity CPU: {cpu}%", + "totalMemory": "Całkowita Pamięć: {memory}" }, "field": { "name": { @@ -4977,15 +4961,15 @@ "label": "Porty" }, "host": { - "label": "" + "label": "Host" } }, "action": { "title": "Akcje", "logs": { - "label": "", + "label": "Wyświetl logi", "page": { - "title": "" + "title": "Logi {name}" } }, "start": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "Nie udało się pobrać kontenerów Dockera" + "internalServerError": "Nie udało się pobrać kontenerów Dockera", + "noColumns": "Wybierz co najmniej jedną kolumnę w ustawieniach widżetu" } }, "kubernetes": { @@ -5380,14 +5365,14 @@ } }, "custom-widgets": { - "label": "", + "label": "Niestandardowe Wigety", "new": { - "label": "" + "label": "Nowy" }, "edit": { - "label": "", + "label": "Edytuj", "flow": { - "label": "" + "label": "Flow" } } }, @@ -5414,7 +5399,7 @@ "docker": { "label": "Docker", "logs": { - "label": "" + "label": "Logi" } }, "kubernetes": { @@ -5525,7 +5510,7 @@ "title": "Polecenia globalne", "option": { "colorScheme": { - "auto": "", + "auto": "Przełącz tryb wyglądu", "light": "Przełącz na tryb jasny", "dark": "Przełącz na tryb ciemny" }, @@ -5566,138 +5551,138 @@ } }, "preferences": { - "title": "", + "title": "Preferencje", "children": { "detail": { - "title": "", - "backHint": "", - "backAction": "" + "title": "Wybierz preferencje do zmiany", + "backHint": "Naciśnij Backspace lub użyj przycisku wstecz, aby wrócić", + "backAction": "Wróć" } }, "notification": { "error": { - "message": "" + "message": "Nie można zaktualizować preferencji" } }, "option": { "preferences": { - "label": "" + "label": "Otwórz preferencje" }, "boolean": { - "enabled": "", - "disabled": "" + "enabled": "Aktywny", + "disabled": "Nieaktywny" }, "board": { - "none": "", - "unavailable": "" + "none": "Brak", + "unavailable": "Aktualnie wybrane (niedostępne)" }, "searchEngine": { - "none": "", - "unavailable": "" + "none": "Brak", + "unavailable": "Aktualnie wybrane (niedostępne)" }, "colorScheme": { - "label": "", + "label": "Paleta kolorów", "children": { "detail": { - "title": "" + "title": "Wybierz swoją palete kolorów" } } }, "locale": { - "label": "", + "label": "Język", "children": { "detail": { - "title": "" + "title": "Wybierz preferowany język" } } }, "defaultSearchEngineId": { - "label": "", + "label": "Domyślna wyszukiwarka", "children": { "detail": { - "title": "" + "title": "Wybierz domyślną wyszukiwarkę" } } }, "openSearchInNewTab": { - "label": "", + "label": "Otwórz wyniki wyszukiwania w nowej karcie", "children": { "detail": { - "title": "" + "title": "Otwórz wyniki wyszukiwania w nowej karcie" } } }, "ddgBangs": { - "label": "", + "label": "Włącz DuckDuckGo bangs", "children": { "detail": { - "title": "" + "title": "Włącz DuckDuckGo bangs" } } }, "firstDayOfWeek": { - "label": "", + "label": "Pierwszy dzień tygodnia", "children": { "detail": { - "title": "" + "title": "Wybierz pierwszy dzień tygodnia" } } }, "homeBoardId": { - "label": "", + "label": "Tablica główna", "children": { "detail": { - "title": "" + "title": "Wybierz swoją tablicę główną" } } }, "mobileHomeBoardId": { - "label": "", + "label": "Mobilna tablica główna", "children": { "detail": { - "title": "" + "title": "Wybierz swoją mobilną tablicę główną" } } }, "pingIconsEnabled": { - "label": "", + "label": "Użyj ikon dla pingów", "children": { "detail": { - "title": "" + "title": "Użyj ikon dla pingów" } } }, "fullPreferencesPage": { - "label": "" + "label": "Otwórz pełną stronę preferencji" } } } } }, "media": { - "help": "", + "help": "Szukaj filmów lub seriali do żądania", "requestMovie": "Poproś o film", "requestSeries": "Poproś o serial", "openIn": "Otwórz w {kind}", "group": { - "title": "" + "title": "Filmy i seriale" }, "action": { "search": { - "label": "", - "description": "", + "label": "Szukaj filmów lub seriali", + "description": "Zacznij wpisywać, aby wyszukać integracje z twoimi żądaniami mediów", "disabled": { - "noIntegration": "", - "scoped": "" + "noIntegration": "Dodaj integrację Overseerr, Jellyseerr lub Seerr, aby włączyć wyszukiwanie multimediów.", + "scoped": "Ten widget nie ma dostępnej integracji z żądaniem mediów." } } }, "availability": { - "available": "", - "partiallyAvailable": "", - "processing": "", - "requested": "", - "pending": "" + "available": "Dostępne", + "partiallyAvailable": "Częściowo", + "processing": "Przetwarzane", + "requested": "Zapytane", + "pending": "Zapytane" } }, "external": { @@ -5705,13 +5690,13 @@ "group": { "searchEngine": { "title": "Wyszukiwarki", - "tipDdgBangs": "", + "tipDdgBangs": "Wskazówka: Możesz również użyć wszystkich DuckDuckGo Bangs tutaj, np. !something", "bang": { - "searchWithQuery": "", - "pressEnterToOpen": "", - "engineSelected": "", - "typeQueryToContinue": "", - "emptyBangHint": "" + "searchWithQuery": "Wyszukaj „{query}” za pomocą {label}", + "pressEnterToOpen": "Naciśnij Enter, aby otworzyć", + "engineSelected": "Wybrano {label} (!{bang})", + "typeQueryToContinue": "Wpisz zapytanie aby kontynuować", + "emptyBangHint": "Wpisz bang np. !yt, a następnie wciśnij Spację, aby wybrać" }, "children": { "action": { @@ -5975,10 +5960,10 @@ "header": { "season": "Sezon", "episodes": "Odcinki", - "status": "" + "status": "Status" }, "status": { - "requested": "" + "requested": "Zapytane" } }, "button": { @@ -6091,69 +6076,69 @@ } }, "onboardingTour": { - "learnMore": "", - "next": "", - "prev": "", - "done": "", - "skip": "", + "learnMore": "Przeczytaj dokumentację", + "next": "Dalej", + "prev": "Wstecz", + "done": "Gotowe", + "skip": "Pomiń samouczek", "manage": { "welcome": { - "title": "", - "description": "" + "title": "Witaj w Homarr!", + "description": "To jest twoje centrum zarządzania. Tutaj zarządzasz tablicami, aplikacjami, integracjami, użytkownikami i ustawieniami serwera. Zacznijmy samouczek z każdej sekcji." }, "boardsList": { - "title": "", - "description": "" + "title": "Twoje Tablice", + "description": "Oto wszystkie twoje Tablice. Każda tablica ma własny układ, tło i ustawienia dostępu. Możesz zobaczyć, kto stworzył każdą z nich i czy jest publiczna czy prywatna." }, "boardsCreate": { - "title": "", - "description": "" + "title": "Utwórz Tablicę", + "description": "Kliknij tutaj, aby utworzyć nową tablicę lub zaimportować go z poprzedniej instalacji Homarr. Każdy dział otrzymuje swój własny adres URL który możesz udostępnić." }, "appsList": { - "title": "", - "description": "" + "title": "Twoje aplikacje", + "description": "To są wszystkie aplikacje, które dodałeś — linki do swoich hostowanych usług. Każda aplikacja ma ikonę, opcjonalną kontrolę zdrowia i odznakę statusu na tablicy." }, "appsCreate": { - "title": "", - "description": "" + "title": "Dodaj Aplikację", + "description": "Zarejestruj nową usługę tutaj. Podaj nazwę, adres URL i ikonę — pojawi się ona jako kafelek na twoich tablicach z monitorowaniem statusu na żywo." }, "integrationsList": { - "title": "", - "description": "" + "title": "Twoje Integracje", + "description": "Są to usługi połączone z Homarr — Sonarr, Radarr, Plex, Jellyfin, i ponad 50 więcej. Wszystkie dane uwierzytelniające są szyfrowane." }, "integrationsCreate": { - "title": "", - "description": "" + "title": "Dodaj Integrację", + "description": "Połącz nową usługę tutaj. Wybierz typ, wprowadź adres URL i klucz API — Homarr sprawdzi połączenie przed zapisaniem." }, "usersList": { - "title": "", - "description": "" + "title": "Twoi Użytkownicy", + "description": "Zobacz wszystkich, którzy mają dostęp. Organizuj użytkowników w grupy i kontroluj, kto może przeglądać lub edytować dane tablice." }, "usersCreate": { - "title": "", - "description": "" + "title": "Utwórz Użytkownika", + "description": "Dodaj tutaj nowe konto użytkownika. Możesz również użyć linków z zaproszeniami do łatwego wdrożenia. To koniec samouczka — wszystko gotowe!" } }, "board": { "welcome": { - "title": "", - "description": "" + "title": "To jest twoja tablica", + "description": "Przeciągnij i upuść tablica, w której układasz aplikacje i widgety. Wciśnij Ctrl+K w dowolnym momencie, aby wyszukać aplikacje, tablice i akcje." }, "editMode": { - "title": "", - "description": "" + "title": "Tryb Edycji", + "description": "Kliknij tutaj, aby przejść do trybu edycji. Następnie możesz dodać widgety, aplikacje i sekcje — przeciągnij aby zmienić pozycję, zmień rozmiar z przeciągając narożnik. Kliknij ponownie, aby zapisać zmiany." }, "settings": { - "title": "", - "description": "" + "title": "Ustawienia Tablicy", + "description": "Zmień nazwę, kolumny, tło, kolory i kto ma dostęp do tablicy. Każda tablica może być publiczna lub prywatna." }, "switcher": { - "title": "", - "description": "" + "title": "Przełącz tablice", + "description": "Przełączaj się między tablicami. Twórz oddzielne tablice dla różnych celów - multimedia, zarządzanie siecią, dedykowane dla użytkownia, lub cokolwiek chcesz." }, "userMenu": { - "title": "", - "description": "" + "title": "Twój Profil", + "description": "Zmień swoją paletę kolorów, język, tablicę startową i preferencje. Możesz również przejść z tąd do obszaru zarządzania." } } } diff --git a/packages/translation/src/lang/pt-br.json b/packages/translation/src/lang/pt-br.json index 2c096f760..1d8b6329e 100644 --- a/packages/translation/src/lang/pt-br.json +++ b/packages/translation/src/lang/pt-br.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "Monitoramento", "speedtest": "Teste de Velocidade", "analytics": "Análises", + "vpn": "", "mediaLibrary": "Biblioteca de mídia", "uptimeMonitoring": "Monitoramento do Uptime" }, @@ -1412,7 +1413,10 @@ "here": "aqui", "iconPicker": { "label": "URL do ícone", - "header": "Digite o nome ou os objetos para filtrar os ícones... O Homarr pesquisará {countIcons} ícones para você." + "header": "Digite o nome ou os objetos para filtrar os ícones... O Homarr pesquisará {countIcons} ícones para você.", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "Alternar para o modo escuro", "switchToLightMode": "Alternar para o modo claro", "management": "Gerenciamento", + "docker": "", "preferences": "Suas preferências", "logout": "Sair", "login": "“Login”", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "Configurações" + "settings": "Configurações", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "Partidas {station}", "noStation": "Nenhuma estação selecionada", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "Estação" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "Mostrar informações do sistema de arquivos" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Aba padrão" }, @@ -2352,6 +2368,9 @@ "name": "Estatísticas do Docker", "description": "Estatísticas dos seus contêineres (Esse widget só pode ser adicionado com privilégios de administrador)", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "Ativar ordenação de itens" }, @@ -2405,6 +2424,12 @@ "title": "Instâncias" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "UPS", "description": "Monitore o status de seus dispositivos UPS através de um servidor NUT", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "Mostrar informações da foto", "description": "Exibir a data da imagem e o índice atual na parte inferior da apresentação" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "Apresentação de imagens em seu álbum Imich", @@ -3133,6 +3162,9 @@ "provider": { "label": "Provedor" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "Identificador", "placeholder": "Nome ou Proprietário/Nome" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "Nenhum provedor", "tooltip": "O provedor não pôde ser analisado, defina-o manualmente após importar as imagens" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "Identificador inválido", "noMatchingVersion": "Nenhuma versão correspondente encontrada", "noReleasesFound": "Nenhum lançamento encontrado", - "noProviderSeleceted": "Nenhum provedor selecionado", - "noProviderResponse": "Nenhuma resposta do provedor" + "noProviderSelected": "", + "noProviderResponse": "Nenhuma resposta do provedor", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "Exibir barra de uso em segundo plano" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "Não encontrado", "text": "Não foi possível encontrar o recurso solicitado" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "Início", @@ -4499,86 +4555,14 @@ "disabled": "Desabilitado" }, "job": { - "minecraftServerStatus": { - "label": "Status do servidor Minecraft" - }, - "timetable": { - "label": "Horários" - }, "iconsUpdater": { "label": "Atualizador de Ícones" }, "analytics": { "label": "Análises" }, - "smartHomeEntityState": { - "label": "Estado de Entidade do Smart Home" - }, "ping": { "label": "Pings" - }, - "mediaServer": { - "label": "Servidor de Mídia" - }, - "mediaOrganizer": { - "label": "Organizadores de Mídia" - }, - "downloads": { - "label": "Downloads" - }, - "mediaRequestStats": { - "label": "Estatísticas de solicitações de mídia" - }, - "mediaRequestList": { - "label": "Lista de solicitações de mídia" - }, - "rssFeeds": { - "label": "Feeds RSS" - }, - "indexerManager": { - "label": "Gerenciador de Indexadores" - }, - "healthMonitoring": { - "label": "Monitoramento de saúde" - }, - "dnsHole": { - "label": "Dados do DNS Hole" - }, - "updateChecker": { - "label": "Verificador de atualizações" - }, - "mediaTranscoding": { - "label": "Transcodificação de mídia" - }, - "networkController": { - "label": "Controlador de rede" - }, - "refreshNotifications": { - "label": "Atualizador de notificações" - }, - "dockerContainers": { - "label": "Contêineres do Docker" - }, - "firewallCpu": { - "label": "CPU do Firewall" - }, - "firewallMemory": { - "label": "Memória do Firewall" - }, - "firewallVersion": { - "label": "Versão do Firewall" - }, - "firewallInterfaces": { - "label": "Interfaces de firewall" - }, - "weather": { - "label": "Clima" - }, - "tracearr": { - "label": "Tracearr" - }, - "umamiActiveVisitors": { - "label": "Visitantes Ativos do Umami" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "Falha ao buscar contêineres Docker" + "internalServerError": "Falha ao buscar contêineres Docker", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/pt.json b/packages/translation/src/lang/pt.json index 48bf163a9..ab6a20c1d 100644 --- a/packages/translation/src/lang/pt.json +++ b/packages/translation/src/lang/pt.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "", "speedtest": "", "analytics": "", + "vpn": "", "mediaLibrary": "", "uptimeMonitoring": "" }, @@ -1412,7 +1413,10 @@ "here": "", "iconPicker": { "label": "", - "header": "" + "header": "", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "", "switchToLightMode": "", "management": "", + "docker": "", "preferences": "", "logout": "", "login": "", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "" + "settings": "", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "", "noStation": "", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -2352,6 +2368,9 @@ "name": "", "description": "", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "" }, @@ -2405,6 +2424,12 @@ "title": "" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "", "description": "" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "", @@ -3133,6 +3162,9 @@ "provider": { "label": "" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "", "placeholder": "" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "", "tooltip": "" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "", "noMatchingVersion": "", "noReleasesFound": "", - "noProviderSeleceted": "", - "noProviderResponse": "" + "noProviderSelected": "", + "noProviderResponse": "", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "", "text": "" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "", @@ -4499,86 +4555,14 @@ "disabled": "" }, "job": { - "minecraftServerStatus": { - "label": "" - }, - "timetable": { - "label": "" - }, "iconsUpdater": { "label": "" }, "analytics": { "label": "" }, - "smartHomeEntityState": { - "label": "" - }, "ping": { "label": "" - }, - "mediaServer": { - "label": "" - }, - "mediaOrganizer": { - "label": "" - }, - "downloads": { - "label": "" - }, - "mediaRequestStats": { - "label": "" - }, - "mediaRequestList": { - "label": "" - }, - "rssFeeds": { - "label": "" - }, - "indexerManager": { - "label": "" - }, - "healthMonitoring": { - "label": "" - }, - "dnsHole": { - "label": "" - }, - "updateChecker": { - "label": "" - }, - "mediaTranscoding": { - "label": "" - }, - "networkController": { - "label": "" - }, - "refreshNotifications": { - "label": "" - }, - "dockerContainers": { - "label": "" - }, - "firewallCpu": { - "label": "" - }, - "firewallMemory": { - "label": "" - }, - "firewallVersion": { - "label": "" - }, - "firewallInterfaces": { - "label": "" - }, - "weather": { - "label": "" - }, - "tracearr": { - "label": "" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "" + "internalServerError": "", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/ro.json b/packages/translation/src/lang/ro.json index 32081e109..d85368be3 100644 --- a/packages/translation/src/lang/ro.json +++ b/packages/translation/src/lang/ro.json @@ -2,168 +2,168 @@ "init": { "step": { "start": { - "title": "", - "subtitle": "", - "description": "", + "title": "Bun venit", + "subtitle": "Să începem cu configurarea Homarr.", + "description": "Pentru a începe, selectează modul în care vrei să setezi Homarr.", "action": { - "scratch": "", - "importOldmarr": "", - "restoreSqlite": "", - "restoreSqliteDescription": "", - "backToOptions": "" + "scratch": "Începe de la zero", + "importOldmarr": "Importă din Homarr înainte de 1.0", + "restoreSqlite": "Restaurează dintr-un backup SQLite", + "restoreSqliteDescription": "Încarcă o copie completă a bazei de date SQLite dintr-o instalare anterioară a Homarr. Aceasta va restaura toate interfețele, utilizatorii, aplicaţiile, integrările şi setările.", + "backToOptions": "← Înapoi la opțiunile de configurare" } }, "import": { - "title": "", - "subtitle": "", + "title": "Importare date", + "subtitle": "Puteți importa date dintr-o instalare Homarr existentă.", "dropzone": { - "title": "", - "description": "" + "title": "Trage fişierul zip aici sau apasă pentru a-l căuta", + "description": "Fișierul zip încărcat va fi procesat și vei putea selecta ceea ce dorești să imporți" }, "fileInfo": { "action": { - "change": "" + "change": "Schimbă fișierul" } }, "importSettings": { - "title": "", - "description": "" + "title": "Importă Setări", + "description": "Configurare comportamentul de importare" }, "boardSelection": { - "title": "", - "description": "", + "title": "S-au găsit {count} secţiuni", + "description": "Alege toate secțiunile cu dimensiunea pe care dorești să o imporți", "action": { - "selectAll": "", - "unselectAll": "" + "selectAll": "Selectează tot", + "unselectAll": "Deselectează tot" } }, "summary": { - "title": "", - "description": "", + "title": "Rezumat importare", + "description": "În rezumatul de mai jos poți vedea ce va fi importat", "action": { - "import": "" + "import": "Confirmă importul şi continuă" }, "entities": { "apps": "Aplicații", "boards": "Planșe", - "integrations": "", - "credentialUsers": "" + "integrations": "Integrări", + "credentialUsers": "Utilizatori acreditați" } }, "tokenModal": { - "title": "", + "title": "Introdu token-ul de import", "field": { "token": { - "label": "", - "description": "" + "label": "Token-ul", + "description": "Introdu token-ul de import afișat în instalarea de homarr anterioară" } }, "notification": { "error": { - "title": "", - "message": "" + "title": "Token invalid", + "message": "Token-ul introdus e invalid" } } } }, "user": { - "title": "", - "subtitle": "", + "title": "Utilizator Administrator", + "subtitle": "Specifică credențialele pentru utilizatorul administrator.", "notification": { "success": { - "title": "", - "message": "" + "title": "Utilizator creat", + "message": "Utilizatorul a fost creat cu succes" }, "error": { - "title": "" + "title": "Crearea utilizatorului a eșuat" } } }, "group": { - "title": "", - "subtitle": "", + "title": "Grup extern", + "subtitle": "Specifică grupul care ar trebui să fie utilizat pentru utilizatori externi.", "form": { "name": { - "label": "", - "description": "" + "label": "Nume grup", + "description": "Numele trebuie să corespundă grupului de administratori al furnizorului extern" } } }, "settings": { "title": "Setări", - "subtitle": "" + "subtitle": "Configurare setări server." }, "integrations": { - "title": "", - "subtitle": "", - "description": "", + "title": "Integrări", + "subtitle": "Selectează serviciile pe care le folosești.", + "description": "Alegeți integrările pe care doriți să le conectați. Vă vom configura tabloul de bord cu widget-uri care se potrivesc automat.", "action": { - "continue": "", - "skip": "" + "continue": "Continuă", + "skip": "Omite pentru moment" }, "configure": { - "progress": "", - "skip": "", - "finishing": "", + "progress": "{current} din {total}", + "skip": "Omite această integrare", + "finishing": "Se configurează tabloul de bord...", "error": { - "title": "", - "message": "" + "title": "Configurarea a eșuat", + "message": "Ceva nu a mers bine în timpul configurării tabloului de bord. Te rugăm să încerci din nou." }, "helpers": { - "tooltip": "", - "description": "" + "tooltip": "Helperi URL", + "description": "Acești helperi încearcă să ghicească URL-ul serviciilor tale. Dacă nu funcționează, introdu adresa manual." } }, "urlTemplate": { - "label": "", - "placeholder": "", - "hint": "", - "warning": "", - "preview": "", + "label": "URL de bază", + "placeholder": "homelab.local sau 192.168.1.1", + "hint": "Introdu domeniul de bază sau IP-ul pentru a completa automat adresele URL ale integrărilor", + "warning": "Setarea adresei URL de bază acum va completa automat toate URL-urile integrărilor, scurtând timpul necesar configurării", + "preview": "de ex. {example}", "mode": { - "subdomain": "", - "hostPort": "" + "subdomain": "Subdomeniu", + "hostPort": "Gazdă:Port" } }, "docker": { - "detected": "", - "detectedBreakdown": "", - "summary": "", - "appBadge": "", - "appSectionLabel": "", - "scanning": "", - "scanningDescription": "" + "detected": "Docker a detectat {count} servicii", + "detectedBreakdown": "{integrations} integrări · {apps} aplicații", + "summary": "Homarr a detectat {apps} aplicații, dintre care {integrations} au integrări încorporate", + "appBadge": "Aplicație", + "appSectionLabel": "Aplicații de adăugat la tabloul de bord ({count} selectate)", + "scanning": "Scanare Docker...", + "scanningDescription": "Căutare containere care rulează pe sistem" } }, "finish": { - "title": "", - "subtitle": "", - "description": "", + "title": "Finalizează configurarea", + "subtitle": "Ești gata să începi!", + "description": "Ai finalizat cu succes procesul de configurare. Acum poți începe să utilizezi Homarr. Selectează următoarea acțiune:", "action": { - "goToBoard": "", - "createBoard": "", - "inviteUser": "", - "docs": "" + "goToBoard": "Mergi la panoul {name}", + "createBoard": "Creează primul tău panou", + "inviteUser": "Invită alți utilizatori", + "docs": "Citește documentația" } } }, - "backToStart": "" + "backToStart": "Înapoi la început" }, "user": { "title": "Utilizatori", "name": "Utilizator", "page": { "login": { - "title": "", - "subtitle": "" + "title": "Conecteaza-te la contul tau", + "subtitle": "Bine ai revenit! Introdu datele de autentificare" }, "invite": { - "title": "", - "subtitle": "", - "description": "" + "title": "Alătură-te Homarr", + "subtitle": "Bine ai venit la Homarr! Te rugăm să-ți creezi contul", + "description": "Ai fost invitat de {username}" }, "init": { - "title": "", + "title": "Instalare Homarr nouă", "subtitle": "" } }, @@ -1021,6 +1021,7 @@ "mediaMonitoring": "", "speedtest": "", "analytics": "", + "vpn": "", "mediaLibrary": "", "uptimeMonitoring": "" }, @@ -1412,7 +1413,10 @@ "here": "", "iconPicker": { "label": "", - "header": "" + "header": "", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "", "switchToLightMode": "", "management": "", + "docker": "", "preferences": "Preferințele dvs.", "logout": "", "login": "Utilizator", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "Setări" + "settings": "Setări", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "", "noStation": "", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "Afișare informații despre sistemul de fișiere" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -2352,6 +2368,9 @@ "name": "", "description": "", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "" }, @@ -2405,6 +2424,12 @@ "title": "" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "", "description": "" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "", @@ -3133,6 +3162,9 @@ "provider": { "label": "" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "", "placeholder": "" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "", "tooltip": "" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "", "noMatchingVersion": "", "noReleasesFound": "", - "noProviderSeleceted": "", - "noProviderResponse": "" + "noProviderSelected": "", + "noProviderResponse": "", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "", "text": "" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "Acasă", @@ -4499,86 +4555,14 @@ "disabled": "" }, "job": { - "minecraftServerStatus": { - "label": "" - }, - "timetable": { - "label": "" - }, "iconsUpdater": { "label": "" }, "analytics": { "label": "" }, - "smartHomeEntityState": { - "label": "" - }, "ping": { "label": "" - }, - "mediaServer": { - "label": "Server-ul media" - }, - "mediaOrganizer": { - "label": "" - }, - "downloads": { - "label": "" - }, - "mediaRequestStats": { - "label": "" - }, - "mediaRequestList": { - "label": "" - }, - "rssFeeds": { - "label": "" - }, - "indexerManager": { - "label": "" - }, - "healthMonitoring": { - "label": "" - }, - "dnsHole": { - "label": "" - }, - "updateChecker": { - "label": "" - }, - "mediaTranscoding": { - "label": "" - }, - "networkController": { - "label": "" - }, - "refreshNotifications": { - "label": "" - }, - "dockerContainers": { - "label": "" - }, - "firewallCpu": { - "label": "" - }, - "firewallMemory": { - "label": "" - }, - "firewallVersion": { - "label": "" - }, - "firewallInterfaces": { - "label": "" - }, - "weather": { - "label": "" - }, - "tracearr": { - "label": "" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "" + "internalServerError": "", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/ru.json b/packages/translation/src/lang/ru.json index edca67abd..62b918327 100644 --- a/packages/translation/src/lang/ru.json +++ b/packages/translation/src/lang/ru.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "", "speedtest": "", "analytics": "", + "vpn": "", "mediaLibrary": "", "uptimeMonitoring": "" }, @@ -1412,7 +1413,10 @@ "here": "здесь", "iconPicker": { "label": "URL иконки", - "header": "Введите название или объекты для фильтрации иконок... Homarr найдёт для вас среди {countIcons} иконок." + "header": "Введите название или объекты для фильтрации иконок... Homarr найдёт для вас среди {countIcons} иконок.", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "Переключить на тёмную тему", "switchToLightMode": "Переключить на светлую тему", "management": "Управление", + "docker": "", "preferences": "Настройки", "logout": "Выйти", "login": "Вход", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "Настройки" + "settings": "Настройки", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "Отправления {station}", "noStation": "Станция не выбрана", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "Станция" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "Показать информацию о файловой системе" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Вкладка по умолчанию" }, @@ -2352,6 +2368,9 @@ "name": "Статистика Docker", "description": "Статистика контейнеров (этот виджет можно добавить только с правами администратора)", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "Включить сортировку элементов" }, @@ -2405,6 +2424,12 @@ "title": "Экземпляры" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "Показать информацию о фото", "description": "Отображать дату снимка и текущий номер в нижней части слайд-шоу" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "Слайд-шоу фотографий из вашего альбома Immich", @@ -3133,6 +3162,9 @@ "provider": { "label": "Провайдер" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "Идентификатор", "placeholder": "Имя или Владелец/Имя" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "Нет провайдера", "tooltip": "Не удалось определить провайдер, укажите его вручную после импорта образов" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "Неверный идентификатор", "noMatchingVersion": "Подходящая версия не найдена", "noReleasesFound": "Релизы не найдены", - "noProviderSeleceted": "Провайдер не выбран", - "noProviderResponse": "Нет ответа от провайдера" + "noProviderSelected": "", + "noProviderResponse": "Нет ответа от провайдера", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "Показывать фоновую шкалу использования" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "Не найдено", "text": "Запрошенный ресурс не найден" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "Главная", @@ -4499,86 +4555,14 @@ "disabled": "Отключено" }, "job": { - "minecraftServerStatus": { - "label": "Статус сервера Minecraft" - }, - "timetable": { - "label": "Расписание" - }, "iconsUpdater": { "label": "Обновление иконок" }, "analytics": { "label": "Аналитика" }, - "smartHomeEntityState": { - "label": "Состояние объектов умного дома" - }, "ping": { "label": "Проверка доступности" - }, - "mediaServer": { - "label": "Медиасервер" - }, - "mediaOrganizer": { - "label": "Организация медиафайлов" - }, - "downloads": { - "label": "Загрузки" - }, - "mediaRequestStats": { - "label": "Статистика запросов медиаконтента" - }, - "mediaRequestList": { - "label": "Список запросов медиаконтента" - }, - "rssFeeds": { - "label": "RSS-ленты" - }, - "indexerManager": { - "label": "Управление индексаторами" - }, - "healthMonitoring": { - "label": "Мониторинг состояния" - }, - "dnsHole": { - "label": "Данные DNS-фильтра" - }, - "updateChecker": { - "label": "Проверка обновлений" - }, - "mediaTranscoding": { - "label": "Перекодирование медиафайлов" - }, - "networkController": { - "label": "Сетевой контроллер" - }, - "refreshNotifications": { - "label": "Обновление уведомлений" - }, - "dockerContainers": { - "label": "Контейнеры Docker" - }, - "firewallCpu": { - "label": "ЦП межсетевого экрана" - }, - "firewallMemory": { - "label": "Память межсетевого экрана" - }, - "firewallVersion": { - "label": "Версия межсетевого экрана" - }, - "firewallInterfaces": { - "label": "Интерфейсы межсетевого экрана" - }, - "weather": { - "label": "Погода" - }, - "tracearr": { - "label": "Tracearr" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "Произошла ошибка при обновлении данных о контейнерах" + "internalServerError": "Произошла ошибка при обновлении данных о контейнерах", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/sk.json b/packages/translation/src/lang/sk.json index 3e44a4be6..a7fbce04f 100644 --- a/packages/translation/src/lang/sk.json +++ b/packages/translation/src/lang/sk.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "", "speedtest": "", "analytics": "", + "vpn": "", "mediaLibrary": "", "uptimeMonitoring": "" }, @@ -1412,7 +1413,10 @@ "here": "tu", "iconPicker": { "label": "URL ikony", - "header": "Zadajte názov alebo objekty na filtrovanie ikon... Homarr pre vás vyhľadá ikony na stránke {countIcons} ." + "header": "Zadajte názov alebo objekty na filtrovanie ikon... Homarr pre vás vyhľadá ikony na stránke {countIcons} .", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "Prepnúť na tmavý motív", "switchToLightMode": "Prepnúť na svetlý motív", "management": "Administrácia", + "docker": "", "preferences": "Vaše preferencie", "logout": "Odhlásiť", "login": "Prihlásiť sa", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "Nastavenia" + "settings": "Nastavenia", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "", "noStation": "", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "Zobraziť informácie o súborovom systéme" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Predvolená karta" }, @@ -2352,6 +2368,9 @@ "name": "Docker stav", "description": "Štatistiky vašich kontajnerov (Túto miniaplikáciu je možné pridať iba s oprávneniami správcu)", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "" }, @@ -2405,6 +2424,12 @@ "title": "" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "", "description": "" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "", @@ -3133,6 +3162,9 @@ "provider": { "label": "Poskytovateľ" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "Identifikátor", "placeholder": "Meno alebo vlastník/meno" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "", "tooltip": "" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "", "noMatchingVersion": "Nenašla sa žiadna zodpovedajúca verzia", "noReleasesFound": "", - "noProviderSeleceted": "", - "noProviderResponse": "" + "noProviderSelected": "", + "noProviderResponse": "", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "Nenájdené", "text": "Nepodarilo sa nájsť požadovaný zdroj" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "Domovská stránka", @@ -4499,86 +4555,14 @@ "disabled": "" }, "job": { - "minecraftServerStatus": { - "label": "Minecraft Server Stav" - }, - "timetable": { - "label": "" - }, "iconsUpdater": { "label": "Aktualizátor ikon" }, "analytics": { "label": "Analytika" }, - "smartHomeEntityState": { - "label": "Stav entity inteligentnej domácnosti" - }, "ping": { "label": "Pingy" - }, - "mediaServer": { - "label": "Multimediálny Server" - }, - "mediaOrganizer": { - "label": "Organizátori médií" - }, - "downloads": { - "label": "Na stiahnutie" - }, - "mediaRequestStats": { - "label": "Štatistiky mediálnych požiadaviek" - }, - "mediaRequestList": { - "label": "Zoznam mediálnych požiadaviek" - }, - "rssFeeds": { - "label": "RSS kanály" - }, - "indexerManager": { - "label": "Správca indexovania" - }, - "healthMonitoring": { - "label": "Monitorovanie zdravia" - }, - "dnsHole": { - "label": "Údaje o dierach DNS" - }, - "updateChecker": { - "label": "Kontrola aktualizácií" - }, - "mediaTranscoding": { - "label": "Prekódovanie médií" - }, - "networkController": { - "label": "Sieťový ovládač" - }, - "refreshNotifications": { - "label": "" - }, - "dockerContainers": { - "label": "Docker kontajnery" - }, - "firewallCpu": { - "label": "" - }, - "firewallMemory": { - "label": "" - }, - "firewallVersion": { - "label": "" - }, - "firewallInterfaces": { - "label": "" - }, - "weather": { - "label": "" - }, - "tracearr": { - "label": "" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "Nepodarilo sa načítať kontajnery Docker" + "internalServerError": "Nepodarilo sa načítať kontajnery Docker", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/sl.json b/packages/translation/src/lang/sl.json index eec31b510..6678f65bd 100644 --- a/packages/translation/src/lang/sl.json +++ b/packages/translation/src/lang/sl.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "", "speedtest": "", "analytics": "", + "vpn": "", "mediaLibrary": "", "uptimeMonitoring": "" }, @@ -1412,7 +1413,10 @@ "here": "", "iconPicker": { "label": "", - "header": "" + "header": "", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "", "switchToLightMode": "Svetli način", "management": "Nastavitve", + "docker": "", "preferences": "Prilagoditev", "logout": "Odjava", "login": "Prijava", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "Nastavitve" + "settings": "Nastavitve", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "", "noStation": "", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -2352,6 +2368,9 @@ "name": "", "description": "", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "" }, @@ -2405,6 +2424,12 @@ "title": "" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "", "description": "" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "", @@ -3133,6 +3162,9 @@ "provider": { "label": "" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "", "placeholder": "" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "", "tooltip": "" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "", "noMatchingVersion": "", "noReleasesFound": "", - "noProviderSeleceted": "", - "noProviderResponse": "" + "noProviderSelected": "", + "noProviderResponse": "", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "", "text": "" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "Domov", @@ -4499,86 +4555,14 @@ "disabled": "" }, "job": { - "minecraftServerStatus": { - "label": "" - }, - "timetable": { - "label": "" - }, "iconsUpdater": { "label": "" }, "analytics": { "label": "" }, - "smartHomeEntityState": { - "label": "" - }, "ping": { "label": "" - }, - "mediaServer": { - "label": "Predstavnostni strežnik" - }, - "mediaOrganizer": { - "label": "" - }, - "downloads": { - "label": "" - }, - "mediaRequestStats": { - "label": "" - }, - "mediaRequestList": { - "label": "" - }, - "rssFeeds": { - "label": "" - }, - "indexerManager": { - "label": "" - }, - "healthMonitoring": { - "label": "" - }, - "dnsHole": { - "label": "" - }, - "updateChecker": { - "label": "" - }, - "mediaTranscoding": { - "label": "" - }, - "networkController": { - "label": "" - }, - "refreshNotifications": { - "label": "" - }, - "dockerContainers": { - "label": "" - }, - "firewallCpu": { - "label": "" - }, - "firewallMemory": { - "label": "" - }, - "firewallVersion": { - "label": "" - }, - "firewallInterfaces": { - "label": "" - }, - "weather": { - "label": "" - }, - "tracearr": { - "label": "" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "" + "internalServerError": "", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/sv.json b/packages/translation/src/lang/sv.json index 1747bc399..7b43c6234 100644 --- a/packages/translation/src/lang/sv.json +++ b/packages/translation/src/lang/sv.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "", "speedtest": "", "analytics": "", + "vpn": "", "mediaLibrary": "", "uptimeMonitoring": "" }, @@ -1412,7 +1413,10 @@ "here": "", "iconPicker": { "label": "", - "header": "" + "header": "", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "Byt till mörkt läge", "switchToLightMode": "Byt till ljust läge", "management": "Administration", + "docker": "", "preferences": "Dina inställningar", "logout": "Logga ut", "login": "Logga in", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "Inställningar" + "settings": "Inställningar", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "", "noStation": "", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "Visa information om filsystemet" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -2352,6 +2368,9 @@ "name": "", "description": "", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "" }, @@ -2405,6 +2424,12 @@ "title": "" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "", "description": "" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "", @@ -3133,6 +3162,9 @@ "provider": { "label": "" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "", "placeholder": "" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "", "tooltip": "" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "", "noMatchingVersion": "", "noReleasesFound": "", - "noProviderSeleceted": "", - "noProviderResponse": "" + "noProviderSelected": "", + "noProviderResponse": "", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "Hittades ej", "text": "" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "Hem", @@ -4499,86 +4555,14 @@ "disabled": "" }, "job": { - "minecraftServerStatus": { - "label": "" - }, - "timetable": { - "label": "" - }, "iconsUpdater": { "label": "" }, "analytics": { "label": "" }, - "smartHomeEntityState": { - "label": "" - }, "ping": { "label": "" - }, - "mediaServer": { - "label": "Mediaserver" - }, - "mediaOrganizer": { - "label": "" - }, - "downloads": { - "label": "" - }, - "mediaRequestStats": { - "label": "" - }, - "mediaRequestList": { - "label": "" - }, - "rssFeeds": { - "label": "" - }, - "indexerManager": { - "label": "" - }, - "healthMonitoring": { - "label": "" - }, - "dnsHole": { - "label": "" - }, - "updateChecker": { - "label": "" - }, - "mediaTranscoding": { - "label": "" - }, - "networkController": { - "label": "" - }, - "refreshNotifications": { - "label": "" - }, - "dockerContainers": { - "label": "" - }, - "firewallCpu": { - "label": "" - }, - "firewallMemory": { - "label": "" - }, - "firewallVersion": { - "label": "" - }, - "firewallInterfaces": { - "label": "" - }, - "weather": { - "label": "" - }, - "tracearr": { - "label": "" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "" + "internalServerError": "", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/tr.json b/packages/translation/src/lang/tr.json index a3acc6650..5755a9519 100644 --- a/packages/translation/src/lang/tr.json +++ b/packages/translation/src/lang/tr.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "", "speedtest": "", "analytics": "", + "vpn": "", "mediaLibrary": "", "uptimeMonitoring": "" }, @@ -1412,7 +1413,10 @@ "here": "buraya", "iconPicker": { "label": "Simge URL'si", - "header": "Simgeleri filtrelemek için isim veya nesne yazın... Homarr sizin için {countIcons} simge arasında arama yapacaktır." + "header": "Simgeleri filtrelemek için isim veya nesne yazın... Homarr sizin için {countIcons} simge arasında arama yapacaktır.", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "Koyu Temaya Geç", "switchToLightMode": "Aydınlık Temaya Geç", "management": "Yönetim", + "docker": "", "preferences": "Tercihleriniz", "logout": "Oturumu kapat", "login": "Giriş", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "Ayarlar" + "settings": "Ayarlar", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "{station} kalkışları", "noStation": "İstasyon seçilmedi", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "İstasyon" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "Dosya Sistemi Bilgilerini Göster" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Varsayılan sekme" }, @@ -2352,6 +2368,9 @@ "name": "Docker istatistikleri", "description": "Konteynerlerinizin istatistikleri (Bu widget yalnızca yönetici ayrıcalıklarıyla eklenebilir)", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "Enable items sorting" }, @@ -2405,6 +2424,12 @@ "title": "Instances" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "Show photo info", "description": "Display date of the picture and the current index at the bottom of the slideshow" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "Slideshow of pictures in your Immich album", @@ -3133,6 +3162,9 @@ "provider": { "label": "Sağlayıcı" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "Tanımlayıcı", "placeholder": "Ad veya Sahip/Ad" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "Sağlayıcı Bulunamadı", "tooltip": "Sağlayıcı çözümlenemedi, lütfen görselleri içe aktardıktan sonra elle ayarlayın" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "Geçersiz tanımlayıcı", "noMatchingVersion": "Eşleşen sürüm bulunamadı", "noReleasesFound": "Hiçbir sürüm bulunamadı", - "noProviderSeleceted": "Hiçbir sağlayıcı seçilmedi", - "noProviderResponse": "Sağlayıcıdan yanıt alınamadı" + "noProviderSelected": "", + "noProviderResponse": "Sağlayıcıdan yanıt alınamadı", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "Show background usage bar" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "Bulunamadı", "text": "İstenen kaynak bulunamadı" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "Ana Sayfa", @@ -4499,86 +4555,14 @@ "disabled": "Pasif" }, "job": { - "minecraftServerStatus": { - "label": "Minecraft sunucu durumu" - }, - "timetable": { - "label": "Zaman Çizelgesi" - }, "iconsUpdater": { "label": "Simge Güncelleme" }, "analytics": { "label": "Analiz" }, - "smartHomeEntityState": { - "label": "Akıllı Ev Varlık Durumu" - }, "ping": { "label": "Pingler" - }, - "mediaServer": { - "label": "Medya Sunucusu" - }, - "mediaOrganizer": { - "label": "Medya Düzenleyicileri" - }, - "downloads": { - "label": "İndirmeler" - }, - "mediaRequestStats": { - "label": "Medya Talep İstatistikleri" - }, - "mediaRequestList": { - "label": "Medya Talep Listesi" - }, - "rssFeeds": { - "label": "RSS Beslemeleri" - }, - "indexerManager": { - "label": "İndeksleyici Yöneticisi" - }, - "healthMonitoring": { - "label": "Sağlık İzleme" - }, - "dnsHole": { - "label": "DNS Çözümleyici Verileri" - }, - "updateChecker": { - "label": "Güncelleme denetleyicisi" - }, - "mediaTranscoding": { - "label": "Medya Kod Dönüştürme" - }, - "networkController": { - "label": "Ağ Denetleyicisi" - }, - "refreshNotifications": { - "label": "Bildirim Güncelleyici" - }, - "dockerContainers": { - "label": "Docker konteynerleri" - }, - "firewallCpu": { - "label": "Güvenlik Duvarı İşlemcisi" - }, - "firewallMemory": { - "label": "Güvenlik Duvarı Belleği" - }, - "firewallVersion": { - "label": "Güvenlik Duvarı Sürümü" - }, - "firewallInterfaces": { - "label": "Güvenlik Duvarı Arayüzleri" - }, - "weather": { - "label": "Hava Durumu" - }, - "tracearr": { - "label": "Tracearr" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "Docker konteynerlerları getirilemedi" + "internalServerError": "Docker konteynerlerları getirilemedi", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/uk.json b/packages/translation/src/lang/uk.json index a6305e4e1..b77603f3d 100644 --- a/packages/translation/src/lang/uk.json +++ b/packages/translation/src/lang/uk.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "", "speedtest": "", "analytics": "", + "vpn": "", "mediaLibrary": "Медіа бібліотека", "uptimeMonitoring": "" }, @@ -1412,7 +1413,10 @@ "here": "тут", "iconPicker": { "label": "", - "header": "" + "header": "", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "Перемикнути на темну тему", "switchToLightMode": "Перемикнути на світлу тему", "management": "Управління", + "docker": "", "preferences": "Ваші налаштування", "logout": "Вийти", "login": "Логін", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "Налаштування" + "settings": "Налаштування", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "", "noStation": "", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "" } @@ -1938,7 +1949,7 @@ "addTable": "Додати таблицю", "deleteTable": "Видалити таблицю", "colorCell": "", - "mergeCell": "", + "mergeCell": "Ввімкнути/вимкнути об'єднання клітини", "addColumnLeft": "", "addColumnRight": "", "deleteColumn": "", @@ -2101,13 +2112,13 @@ "label": "" }, "1y": { - "label": "" + "label": "1 рік" }, "2y": { "label": "" }, "5y": { - "label": "" + "label": "5 років" }, "10y": { "label": "" @@ -2142,7 +2153,7 @@ "label": "" }, "1mo": { - "label": "" + "label": "1 місяць" } } } @@ -2182,7 +2193,7 @@ "label": "Температура за Фаренгейтом" }, "disableTemperatureDecimals": { - "label": "" + "label": "Вимкнути десяткові дроби температури" }, "showCurrentWindSpeed": { "label": "Показати поточну швидкість вітру", @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -2352,6 +2368,9 @@ "name": "", "description": "", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "" }, @@ -2359,7 +2378,7 @@ "label": "", "option": { "name": "", - "state": "", + "state": "Стан", "cpuUsage": "", "memoryUsage": "" } @@ -2405,6 +2424,12 @@ "title": "" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2470,7 +2495,7 @@ "videoDecision": { "directPlay": "", "transcode": "", - "directStream": "" + "directStream": "Прямий потік" } }, "recentActivity": { @@ -2550,7 +2575,7 @@ "label": "", "description": "", "option": { - "chart": "", + "chart": "Графік", "events": "", "topPages": "", "topReferrers": "" @@ -2701,7 +2726,7 @@ "description": "" }, "showInboxRing": { - "label": "", + "label": "Показувати кільце вхідних повідомлень", "description": "" }, "showDocumentsTotal": { @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "", "description": "" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "", @@ -2895,7 +2924,7 @@ "failed": "Невдало", "processing": "В обробці", "leeching": "Завантажується", - "stalled": "", + "stalled": "Зупинено", "unknown": "Невідомо", "seeding": "Роздається" }, @@ -3108,7 +3137,7 @@ "label": "" }, "showOnlyIcon": { - "label": "" + "label": "Показати тільки іконку" }, "topReleases": { "label": "", @@ -3133,6 +3162,9 @@ "provider": { "label": "" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "", "placeholder": "" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "", "tooltip": "" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3195,15 +3234,16 @@ "openReleasePage": "", "releaseDescription": "", "projectDescription": "", - "created": "", + "created": "Створено", "error": { "label": "", "messages": { "invalidIdentifier": "", "noMatchingVersion": "", "noReleasesFound": "", - "noProviderSeleceted": "", - "noProviderResponse": "" + "noProviderSelected": "", + "noProviderResponse": "", + "unexpected": "" } } }, @@ -3315,7 +3355,7 @@ "label": "" } }, - "latestResult": "", + "latestResult": "Останній результат", "averages": "", "tests": "", "recentResults": "", @@ -3402,7 +3442,7 @@ "description": "" }, "maxNowPlayingItems": { - "label": "", + "label": "Максимум елементів для відтворення", "description": "" }, "showLibraryCount": { @@ -3497,12 +3537,17 @@ "label": "" }, "free": { - "label": "" + "label": "Відсоток вільного простору (напр., 24% вільно)" } } }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3552,19 +3597,19 @@ "label": "" }, "showGpu": { - "label": "" + "label": "Показати графічний процесор" }, "showLoadAvg": { - "label": "" + "label": "Показати середній рівень навантаження" }, "showNet": { - "label": "" + "label": "Показати мережу" }, "showTemp": { - "label": "" + "label": "Показувати температуру" }, "showBattery": { - "label": "" + "label": "Показувати заряд батереї" }, "showServices": { "label": "" @@ -3578,7 +3623,7 @@ }, "noRecords": "", "error": { - "internalServerError": "" + "internalServerError": "Не вдається підключитися до Beszel. Перевірте параметри інтеграції." }, "column": { "system": "", @@ -3630,7 +3675,7 @@ "label": "" }, "showUptime": { - "label": "" + "label": "Показати час роботи" }, "showAgent": { "label": "" @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3648,7 +3696,7 @@ "net": "", "temp": "", "battery": "", - "services": "", + "services": "Служби", "uptime": "", "agent": "" } @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -3728,12 +3779,12 @@ "title": "", "subtitle": "", "series": "", - "cache": "" + "cache": "Кеш/Баффери" }, "disk": { "title": "", "subtitle": "", - "series": "" + "series": "Використано" }, "diskIO": { "title": "", @@ -3930,7 +3981,7 @@ }, "contain": { "label": "", - "description": "" + "description": "Масштабує зображення максимально великою в межах контейнера без обрізання або розтягування зображення." } } }, @@ -4133,6 +4184,11 @@ "title": "Не знайдено", "text": "Не вдалося знайти запитуваний ресурс" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "Головна", @@ -4240,7 +4296,7 @@ "private": "Ця дошка є приватною" }, "noResults": { - "title": "" + "title": "У вас ще немає дошок" }, "modal": { "createBoard": { @@ -4499,86 +4555,14 @@ "disabled": "" }, "job": { - "minecraftServerStatus": { - "label": "Статус сервера Minecraft" - }, - "timetable": { - "label": "" - }, "iconsUpdater": { "label": "" }, "analytics": { "label": "Аналітика" }, - "smartHomeEntityState": { - "label": "" - }, "ping": { "label": "Пінги" - }, - "mediaServer": { - "label": "Медіа сервер" - }, - "mediaOrganizer": { - "label": "" - }, - "downloads": { - "label": "Завантаження" - }, - "mediaRequestStats": { - "label": "" - }, - "mediaRequestList": { - "label": "" - }, - "rssFeeds": { - "label": "" - }, - "indexerManager": { - "label": "" - }, - "healthMonitoring": { - "label": "" - }, - "dnsHole": { - "label": "" - }, - "updateChecker": { - "label": "" - }, - "mediaTranscoding": { - "label": "" - }, - "networkController": { - "label": "" - }, - "refreshNotifications": { - "label": "" - }, - "dockerContainers": { - "label": "" - }, - "firewallCpu": { - "label": "" - }, - "firewallMemory": { - "label": "" - }, - "firewallVersion": { - "label": "" - }, - "firewallInterfaces": { - "label": "" - }, - "weather": { - "label": "" - }, - "tracearr": { - "label": "" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -4681,7 +4665,7 @@ "table": { "header": { "id": "ІДЕНТИФІКАТОР", - "createdBy": "", + "createdBy": "Створено", "actions": "" } } @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "" + "internalServerError": "", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/vi.json b/packages/translation/src/lang/vi.json index dd8e8b519..36207f3d7 100644 --- a/packages/translation/src/lang/vi.json +++ b/packages/translation/src/lang/vi.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "", "speedtest": "", "analytics": "", + "vpn": "", "mediaLibrary": "", "uptimeMonitoring": "" }, @@ -1412,7 +1413,10 @@ "here": "", "iconPicker": { "label": "", - "header": "" + "header": "", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "", "switchToLightMode": "Đổi qua chế độ nền sáng", "management": "", + "docker": "", "preferences": "Cá nhân hoá", "logout": "", "login": "Đăng nhập", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "Cài đặt" + "settings": "Cài đặt", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "", "noStation": "", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -2352,6 +2368,9 @@ "name": "", "description": "", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "" }, @@ -2405,6 +2424,12 @@ "title": "" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "", "description": "" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "", @@ -3133,6 +3162,9 @@ "provider": { "label": "" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "", "placeholder": "" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "", "tooltip": "" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "", "noMatchingVersion": "", "noReleasesFound": "", - "noProviderSeleceted": "", - "noProviderResponse": "" + "noProviderSelected": "", + "noProviderResponse": "", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "", "text": "" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "Trang chủ", @@ -4499,86 +4555,14 @@ "disabled": "" }, "job": { - "minecraftServerStatus": { - "label": "" - }, - "timetable": { - "label": "" - }, "iconsUpdater": { "label": "" }, "analytics": { "label": "" }, - "smartHomeEntityState": { - "label": "" - }, "ping": { "label": "" - }, - "mediaServer": { - "label": "Máy chủ đa phương tiện" - }, - "mediaOrganizer": { - "label": "" - }, - "downloads": { - "label": "" - }, - "mediaRequestStats": { - "label": "" - }, - "mediaRequestList": { - "label": "" - }, - "rssFeeds": { - "label": "" - }, - "indexerManager": { - "label": "" - }, - "healthMonitoring": { - "label": "" - }, - "dnsHole": { - "label": "" - }, - "updateChecker": { - "label": "" - }, - "mediaTranscoding": { - "label": "" - }, - "networkController": { - "label": "" - }, - "refreshNotifications": { - "label": "" - }, - "dockerContainers": { - "label": "" - }, - "firewallCpu": { - "label": "" - }, - "firewallMemory": { - "label": "" - }, - "firewallVersion": { - "label": "" - }, - "firewallInterfaces": { - "label": "" - }, - "weather": { - "label": "" - }, - "tracearr": { - "label": "" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "" + "internalServerError": "", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/translation/src/lang/zh.json b/packages/translation/src/lang/zh.json index 99def3d69..bfa7bcc0f 100644 --- a/packages/translation/src/lang/zh.json +++ b/packages/translation/src/lang/zh.json @@ -1021,6 +1021,7 @@ "mediaMonitoring": "", "speedtest": "", "analytics": "", + "vpn": "", "mediaLibrary": "", "uptimeMonitoring": "" }, @@ -1412,7 +1413,10 @@ "here": "此處", "iconPicker": { "label": "Icon 網址", - "header": "輸入名稱或物件來篩選 icons,Homarr 會自動搜尋 {countIcons}" + "header": "輸入名稱或物件來篩選 icons,Homarr 會自動搜尋 {countIcons}", + "headerLoading": "", + "noResults": "", + "error": "" }, "colorScheme": { "options": { @@ -1464,6 +1468,7 @@ "switchToDarkMode": "切換至暗黑模式", "switchToLightMode": "切換至日間模式", "management": "管理中心", + "docker": "", "preferences": "偏好設定", "logout": "登出", "login": "登入", @@ -1593,7 +1598,10 @@ }, "menu": { "label": { - "settings": "設定" + "settings": "設定", + "refresh": "", + "options": "", + "actions": "" } }, "create": { @@ -1895,6 +1903,9 @@ "title": "", "noStation": "", "option": { + "baseUrl": { + "label": "" + }, "station": { "label": "" } @@ -2273,6 +2284,11 @@ "fileSystem": { "label": "顯示檔案系統訊息" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "預設頁面" }, @@ -2352,6 +2368,9 @@ "name": "", "description": "", "option": { + "columns": { + "label": "" + }, "enableRowSorting": { "label": "" }, @@ -2405,6 +2424,12 @@ "title": "" } }, + "vpn": { + "name": "", + "description": "", + "serviceUnavailable": "", + "option": {} + }, "ups": { "name": "", "description": "", @@ -2769,6 +2794,10 @@ "showPhotoInfo": { "label": "", "description": "" + }, + "randomizePhotos": { + "label": "", + "description": "" } }, "description": "", @@ -3133,6 +3162,9 @@ "provider": { "label": "提供者" }, + "providerUrl": { + "label": "" + }, "identifier": { "label": "識別碼", "placeholder": "名稱或擁有者/名稱" @@ -3180,6 +3212,13 @@ "noProvider": { "label": "", "tooltip": "" + }, + "tokens": { + "label": "", + "token": "", + "configured": "", + "notConfigured": "", + "save": "" } } }, @@ -3202,8 +3241,9 @@ "invalidIdentifier": "", "noMatchingVersion": "找不到匹配的版本", "noReleasesFound": "", - "noProviderSeleceted": "", - "noProviderResponse": "" + "noProviderSelected": "", + "noProviderResponse": "", + "unexpected": "" } } }, @@ -3503,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3639,6 +3684,9 @@ "error": { "internalServerError": "" }, + "empty": { + "noSystems": "" + }, "metric": { "cpu": "", "memory": "", @@ -3712,6 +3760,9 @@ } }, "selectSystem": "", + "empty": { + "noSystems": "" + }, "error": { "internalServerError": "", "liveConnectionFailed": "", @@ -4133,6 +4184,11 @@ "title": "不存在", "text": "找不到請求的資源" }, + "error": { + "title": "", + "text": "", + "digest": "" + }, "navbar": { "items": { "home": "首頁", @@ -4499,86 +4555,14 @@ "disabled": "" }, "job": { - "minecraftServerStatus": { - "label": "Minecraft 伺服器狀態" - }, - "timetable": { - "label": "" - }, "iconsUpdater": { "label": "icon 上傳器" }, "analytics": { "label": "分析" }, - "smartHomeEntityState": { - "label": "智能居家狀態" - }, "ping": { "label": "Pings" - }, - "mediaServer": { - "label": "多媒體伺服器" - }, - "mediaOrganizer": { - "label": "多媒體組織者" - }, - "downloads": { - "label": "正在下載" - }, - "mediaRequestStats": { - "label": "多媒體請求狀態" - }, - "mediaRequestList": { - "label": "多媒體請求列表" - }, - "rssFeeds": { - "label": "RSS 訂閱" - }, - "indexerManager": { - "label": "索引器管理" - }, - "healthMonitoring": { - "label": "健康監測" - }, - "dnsHole": { - "label": "DNS Hole 數據" - }, - "updateChecker": { - "label": "更新檢查" - }, - "mediaTranscoding": { - "label": "多媒體轉碼中" - }, - "networkController": { - "label": "網路控制" - }, - "refreshNotifications": { - "label": "" - }, - "dockerContainers": { - "label": "" - }, - "firewallCpu": { - "label": "" - }, - "firewallMemory": { - "label": "" - }, - "firewallVersion": { - "label": "" - }, - "firewallInterfaces": { - "label": "" - }, - "weather": { - "label": "" - }, - "tracearr": { - "label": "" - }, - "umamiActiveVisitors": { - "label": "" } }, "interval": { @@ -5071,7 +5055,8 @@ } }, "error": { - "internalServerError": "取得 Docker 容器失敗" + "internalServerError": "取得 Docker 容器失敗", + "noColumns": "" } }, "kubernetes": { diff --git a/packages/validation/src/email.ts b/packages/validation/src/email.ts new file mode 100644 index 000000000..fd076a3a8 --- /dev/null +++ b/packages/validation/src/email.ts @@ -0,0 +1,16 @@ +import { z } from "zod/v4"; + +export const utf8EmailSchema = z.email({ pattern: z.regexes.unicodeEmail }); + +export const optionalEmailSchema = utf8EmailSchema.or(z.string().length(0)).optional(); + +export const nullableEmailSchema = utf8EmailSchema + .or(z.literal("")) + .transform((value) => (value === "" ? null : value)) + .optional() + .nullable(); + +export const requiredNullableEmailSchema = utf8EmailSchema + .or(z.literal("")) + .transform((value) => (value === "" ? null : value)) + .nullable(); diff --git a/packages/validation/src/form/i18n.spec.ts b/packages/validation/src/form/i18n.spec.ts index 3ff2cba94..9232ecf91 100644 --- a/packages/validation/src/form/i18n.spec.ts +++ b/packages/validation/src/form/i18n.spec.ts @@ -4,6 +4,7 @@ import { z } from "zod/v4"; import type { TranslationFunction } from "@homarr/translation"; import { createCustomErrorParams, zodErrorMap } from "./i18n"; +import { utf8EmailSchema } from "../email"; const expectError = (error: z.core.$ZodIssue, key: string) => { expect(error.message).toContain(key); @@ -111,4 +112,19 @@ describe("i18n", () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion expectError(result.error.issues[0]!, "boardAlreadyExists"); }); + + test("should validate UTF8SMTP email with umlauts", () => { + const result = utf8EmailSchema.safeParse("jörg@example.com"); + expect(result.success).toBe(true); + }); + + test("should validate UTF8SMTP email with unicode domain", () => { + const result = utf8EmailSchema.safeParse("user@münchen.de"); + expect(result.success).toBe(true); + }); + + test("should validate standard ASCII emails", () => { + const result = utf8EmailSchema.safeParse("user@example.com"); + expect(result.success).toBe(true); + }); }); diff --git a/packages/validation/src/user.ts b/packages/validation/src/user.ts index 824567fa6..b63e18b68 100644 --- a/packages/validation/src/user.ts +++ b/packages/validation/src/user.ts @@ -6,6 +6,7 @@ import type { TranslationObject } from "@homarr/translation"; import { zodEnumFromArray } from "./enums"; import { createCustomErrorParams } from "./form/i18n"; +import { nullableEmailSchema, optionalEmailSchema } from "./email"; // We always want the lowercase version of the username to compare it in a case-insensitive way export const usernameSchema = z.string().trim().toLowerCase().min(3).max(255); @@ -42,7 +43,7 @@ export const userBaseCreateSchema = z.object({ username: usernameSchema, password: userPasswordSchema, confirmPassword: z.string(), - email: z.string().email().or(z.string().length(0)).optional(), + email: optionalEmailSchema, }); export const userCreateSchema = addConfirmPasswordRefinement(userBaseCreateSchema).and( @@ -85,13 +86,7 @@ export const userRegistrationApiSchema = userRegistrationSchema.and( export const userEditProfileSchema = z.object({ id: z.string(), name: usernameSchema, - email: z - .string() - .email() - .or(z.literal("")) - .transform((value) => (value === "" ? null : value)) - .optional() - .nullable(), + email: nullableEmailSchema, }); const baseChangePasswordSchema = z.object({ diff --git a/packages/widgets/src/_inputs/common.tsx b/packages/widgets/src/_inputs/common.tsx index 5c7769eb1..3d575bfb0 100644 --- a/packages/widgets/src/_inputs/common.tsx +++ b/packages/widgets/src/_inputs/common.tsx @@ -8,9 +8,10 @@ export interface CommonWidgetInputProps { property: string; options: Omit, "defaultValue" | "type">; initialOptions: Record; + itemId?: string; } -type UseWidgetInputTranslationReturnType = (key: "label" | "description") => string; +type UseWidgetInputTranslationReturnType = (key: "label" | "description" | "placeholder") => string; /** * Short description why as and unknown convertions are used below: diff --git a/packages/widgets/src/_inputs/index.ts b/packages/widgets/src/_inputs/index.ts index ac9ef1a67..2e6943ac0 100644 --- a/packages/widgets/src/_inputs/index.ts +++ b/packages/widgets/src/_inputs/index.ts @@ -17,6 +17,7 @@ import { WidgetUmamiEventInput } from "./widget-umami-event-input"; import { WidgetUmamiEventNamesInput } from "./widget-umami-event-names-input"; import { WidgetUmamiWebsiteInput } from "./widget-umami-website-input"; import { WidgetIntegrationSelectInput } from "./widget-integration-select-input"; +import { WidgetIntegrationMultiSelectInput } from "./widget-integration-multi-select-input"; const mapping = { anchorNote: WidgetAnchorNoteInput, @@ -37,6 +38,7 @@ const mapping = { umamiWebsite: WidgetUmamiWebsiteInput, customWidgetSelect: WidgetCustomWidgetSelectInput, integrationSelect: WidgetIntegrationSelectInput, + integrationMultiSelect: WidgetIntegrationMultiSelectInput, } satisfies Record; export const getInputForType = (type: TType) => { diff --git a/packages/widgets/src/_inputs/widget-anchor-note-input.tsx b/packages/widgets/src/_inputs/widget-anchor-note-input.tsx index e045e7df0..e909477b5 100644 --- a/packages/widgets/src/_inputs/widget-anchor-note-input.tsx +++ b/packages/widgets/src/_inputs/widget-anchor-note-input.tsx @@ -30,9 +30,6 @@ export const WidgetAnchorNoteInput = ({ property, kind, options }: CommonWidgetI { enabled: Boolean(integrationId), refetchOnMount: false, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, }, ); diff --git a/packages/widgets/src/_inputs/widget-dynamic-select-input.tsx b/packages/widgets/src/_inputs/widget-dynamic-select-input.tsx index de8ef5021..3b5b636aa 100644 --- a/packages/widgets/src/_inputs/widget-dynamic-select-input.tsx +++ b/packages/widgets/src/_inputs/widget-dynamic-select-input.tsx @@ -24,7 +24,11 @@ export const WidgetDynamicSelectInput = ({ property, kind, options }: CommonWidg const inputProps = form.getInputProps(`options.${property}`); const [search, setSearch] = useState(""); const [debouncedSearch] = useDebouncedValue(search, 300); - const { isPending, options: selectOptions } = options.useOptions(debouncedSearch, form.values.integrationIds); + const { isPending, options: selectOptions } = options.useOptions( + debouncedSearch, + form.values.integrationIds, + form.values.options, + ); const currentOption = inputProps.value as DynamicSelectOption | null; const onChange = inputProps.onChange as (value: DynamicSelectOption | null) => void; diff --git a/packages/widgets/src/_inputs/widget-integration-multi-select-input.tsx b/packages/widgets/src/_inputs/widget-integration-multi-select-input.tsx new file mode 100644 index 000000000..f2fa0b542 --- /dev/null +++ b/packages/widgets/src/_inputs/widget-integration-multi-select-input.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { MultiSelect, Text } from "@mantine/core"; + +import { useScopedI18n } from "@homarr/translation/client"; + +import type { CommonWidgetInputProps } from "./common"; +import { useWidgetInputTranslation } from "./common"; +import { useFormContext } from "./form"; + +const useIntegrationSelectTranslation = () => useScopedI18n("widget.integrationSelect"); + +export const WidgetIntegrationMultiSelectInput = ({ + property, + kind, + options, +}: CommonWidgetInputProps<"integrationMultiSelect">) => { + const t = useIntegrationSelectTranslation(); + const tInput = useWidgetInputTranslation(kind, property); + const form = useFormContext(); + + const integrationIds = form.values.integrationIds as string[]; + const inputProps = form.getInputProps(`options.${property}`); + const selectedValues = (inputProps.value as string[] | undefined) ?? []; + + const { data: selectData = [], isPending, isError } = options.useOptions(integrationIds); + const data = [ + ...selectData, + ...selectedValues + .filter((selectedValue) => !selectData.some((option) => option.value === selectedValue)) + .map((selectedValue) => ({ value: selectedValue, label: selectedValue })), + ]; + + if (integrationIds.length === 0) { + return ( + + {t("configureIntegrationFirst")} + + ); + } + + const description = options.withDescription ? tInput("description") : undefined; + const placeholder = isPending ? t("loading") : tInput("placeholder"); + const loadError = isError ? t("loadError") : undefined; + + return ( + + ); +}; diff --git a/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx b/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx index f7f026c3c..45c9ed595 100644 --- a/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx +++ b/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx @@ -12,6 +12,7 @@ import { Group, Image, Loader, + PasswordInput, Select, Stack, Text, @@ -27,6 +28,7 @@ import { IconCopy, IconCopyCheckFilled, IconEdit, + IconKey, IconPackageImport, IconPlus, IconTrash, @@ -38,16 +40,21 @@ import { escapeForRegEx } from "@tiptap/react"; import { clientApi } from "@homarr/api/client"; import { useSession } from "@homarr/auth/client"; import { createId } from "@homarr/common"; -import { getIconUrl } from "@homarr/definitions"; -import type { IntegrationKind } from "@homarr/definitions"; +import type { ReleaseProviderKind } from "@homarr/definitions"; +import { + getReleaseProviderDefaultUrl, + getReleaseProviderIconUrl, + getReleaseProviderName, + normalizeReleaseProviderIdentifier, + releaseProviderKinds, +} from "@homarr/definitions"; import { findBestIconMatch, IconPicker } from "@homarr/forms-collection"; import { createModal, useModalAction } from "@homarr/modals"; +import { showErrorNotification } from "@homarr/notifications"; import { useScopedI18n } from "@homarr/translation/client"; import { MaskedImage } from "@homarr/ui"; import type { ReleasesRepository, ReleasesVersionFilter } from "../releases/releases-repository"; -import { WidgetIntegrationSelect } from "../widget-integration-select"; -import type { IntegrationSelectOption } from "../widget-integration-select"; import type { CommonWidgetInputProps } from "./common"; import { useWidgetInputTranslation } from "./common"; import { useFormContext } from "./form"; @@ -57,13 +64,10 @@ interface FormValidation { errors: FormErrors; } -interface Integration extends IntegrationSelectOption { - iconUrl: string; -} - export const WidgetMultiReleasesRepositoriesInput = ({ property, kind, + itemId, }: CommonWidgetInputProps<"multiReleasesRepositories">) => { const t = useWidgetInputTranslation(kind, property); const tRepository = useScopedI18n("widget.releases.option.repositories"); @@ -78,38 +82,19 @@ export const WidgetMultiReleasesRepositoriesInput = ({ const { data: session } = useSession(); const isAdmin = session?.user.permissions.includes("admin") ?? false; - const integrationsApi = clientApi.integration.allOfGivenCategory.useQuery( - { - category: "releasesProvider", - }, - { - refetchOnMount: false, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - }, - ); - const integrations = useMemo( - () => - integrationsApi.data?.reduce>((acc, integration) => { - acc[integration.id] = { - id: integration.id, - name: integration.name, - url: integration.url, - kind: integration.kind, - iconUrl: getIconUrl(integration.kind), - }; - return acc; - }, {}) ?? {}, - [integrationsApi], + const providerOptions = useMemo( + () => releaseProviderKinds.map((provider) => ({ value: provider, label: getReleaseProviderName(provider) })), + [], ); const onRepositorySave = useCallback( (repository: ReleasesRepository, index: number): FormValidation => { - form.setFieldValue(`options.${property}.${index}.providerIntegrationId`, repository.providerIntegrationId); + form.setFieldValue(`options.${property}.${index}.provider`, repository.provider); form.setFieldValue(`options.${property}.${index}.identifier`, repository.identifier); form.setFieldValue(`options.${property}.${index}.name`, repository.name); form.setFieldValue(`options.${property}.${index}.versionFilter`, repository.versionFilter); form.setFieldValue(`options.${property}.${index}.iconUrl`, repository.iconUrl); + form.setFieldValue(`options.${property}.${index}.providerUrl`, repository.providerUrl); const formValidation = form.validate(); const fieldErrors: FormErrors = Object.entries(formValidation.errors).reduce((acc, [key, value]) => { @@ -152,7 +137,7 @@ export const WidgetMultiReleasesRepositoriesInput = ({ onRepositorySave: (saved) => onRepositorySave(saved, index), onRepositoryCancel: () => onRepositoryRemove(index), versionFilterPrecisionOptions, - integrations, + providerOptions, }); }; @@ -183,7 +168,6 @@ export const WidgetMultiReleasesRepositoriesInput = ({ onClick={() => openImportModal({ repositories, - integrations, versionFilterPrecisionOptions, onConfirm: (selectedRepositories) => { if (!selectedRepositories.length) return; @@ -210,22 +194,22 @@ export const WidgetMultiReleasesRepositoriesInput = ({ {repositories.map((repository, index) => { - const integration = repository.providerIntegrationId - ? integrations[repository.providerIntegrationId] - : undefined; + const providerName = repository.provider ? getReleaseProviderName(repository.provider) : ""; return ( - - {integration?.name ?? ""} + + {providerName} @@ -242,7 +226,7 @@ export const WidgetMultiReleasesRepositoriesInput = ({ repository, onRepositorySave: (saved) => onRepositorySave(saved, index), versionFilterPrecisionOptions, - integrations, + providerOptions, }) } variant="light" @@ -269,10 +253,141 @@ export const WidgetMultiReleasesRepositoriesInput = ({ ); })} + {itemId && } ); }; +const providersWithAuth: ReleaseProviderKind[] = [ + "github", + "gitHubContainerRegistry", + "dockerHub", + "gitlab", + "npm", + "codeberg", +]; + +const ProviderTokensSection = ({ itemId, repositories }: { itemId: string; repositories: ReleasesRepository[] }) => { + const tRepository = useScopedI18n("widget.releases.option.repositories"); + const { data: configuredKinds = [], refetch } = clientApi.widget.secrets.getConfiguredKinds.useQuery({ itemId }); + const setSecret = clientApi.widget.secrets.setSecret.useMutation({ + onSuccess: () => refetch(), + onError: (error) => { + showErrorNotification({ + title: "Failed to save token", + message: error.message, + }); + }, + }); + const deleteSecret = clientApi.widget.secrets.deleteSecret.useMutation({ + onSuccess: () => refetch(), + onError: (error) => { + showErrorNotification({ + title: "Failed to delete token", + message: error.message, + }); + }, + }); + + const authProviders = useMemo(() => { + const usedProviders = repositories.map((r) => r.provider).filter((p): p is ReleaseProviderKind => p !== undefined); + const configuredProviders = configuredKinds.filter((k): k is ReleaseProviderKind => + providersWithAuth.includes(k as ReleaseProviderKind), + ); + return [...new Set([...usedProviders, ...configuredProviders])].filter((p) => providersWithAuth.includes(p)); + }, [repositories, configuredKinds]); + + if (authProviders.length === 0) return null; + + return ( + <> + + + {authProviders.map((provider) => ( + { + await setSecret.mutateAsync({ itemId, kind: provider, value }); + }} + onDelete={async () => { + await deleteSecret.mutateAsync({ itemId, kind: provider }); + }} + /> + ))} + + + ); +}; + +const ProviderTokenInput = ({ + provider, + hasToken, + onSave, + onDelete, +}: { + provider: ReleaseProviderKind; + hasToken: boolean; + onSave: (value: string) => Promise; + onDelete: () => Promise; +}) => { + const tRepository = useScopedI18n("widget.releases.option.repositories"); + const [value, setValue] = useState(""); + const [editing, setEditing] = useState(false); + const [saving, setSaving] = useState(false); + + const handleSave = async () => { + if (!value.trim()) return; + setSaving(true); + try { + await onSave(value.trim()); + setValue(""); + setEditing(false); + } finally { + setSaving(false); + } + }; + + const handleDelete = async () => { + setSaving(true); + try { + await onDelete(); + setValue(""); + setEditing(false); + } finally { + setSaving(false); + } + }; + + return ( + + { + setValue(e.currentTarget.value); + setEditing(true); + }} + style={{ flex: 1 }} + size="xs" + leftSection={} + /> + {editing && value.trim() && ( + + )} + {hasToken && !editing && ( + + + + )} + + ); +}; + const formatVersionFilterRegex = (versionFilter: ReleasesVersionFilter | undefined) => { if (!versionFilter) return undefined; @@ -294,7 +409,7 @@ interface RepositoryEditProps { onRepositorySave: (repository: ReleasesRepository) => FormValidation; onRepositoryCancel?: () => void; versionFilterPrecisionOptions: string[]; - integrations: Record; + providerOptions: { value: ReleaseProviderKind; label: string }[]; } const RepositoryEditModal = createModal(({ innerProps, actions }) => { @@ -302,11 +417,6 @@ const RepositoryEditModal = createModal(({ innerProps, acti const [loading, setLoading] = useState(false); const [tempRepository, setTempRepository] = useState(() => ({ ...innerProps.repository })); const [formErrors, setFormErrors] = useState({}); - const integrationSelectOptions: IntegrationSelectOption[] = useMemo( - () => Object.values(innerProps.integrations), - [innerProps.integrations], - ); - // Allows user to not select an icon by removing the url from the input, // will only try and get an icon if the name or identifier changes const [autoSetIcon, setAutoSetIcon] = useState(false); @@ -361,15 +471,14 @@ const RepositoryEditModal = createModal(({ innerProps, acti
- { - handleChange({ providerIntegrationId: value.length > 0 ? value.pop() : undefined }); + handleChange({ provider: value ? (value as ReleaseProviderKind) : undefined }); }} />
@@ -397,6 +506,16 @@ const RepositoryEditModal = createModal(({ innerProps, acti />
+ { + handleChange({ providerUrl: event.currentTarget.value || undefined }); + }} + error={formErrors[`${innerProps.fieldPath}.providerUrl`]} + /> + void; @@ -522,12 +640,12 @@ interface ImportRepositorySelectProps { const ImportRepositorySelect = ({ repository, checked, - integration, versionFilterPrecisionOptions, disabled = false, onImageSelectionChanged = undefined, }: ImportRepositorySelectProps) => { const tRepository = useScopedI18n("widget.releases.option.repositories"); + const provider = repository.provider; return ( @@ -570,12 +688,12 @@ const ImportRepositorySelect = ({ )} - + - {integration ? ( + {provider ? ( - {integration?.name ?? tRepository("noProvider.label")} + {provider ? getReleaseProviderName(provider) : tRepository("noProvider.label")} @@ -596,7 +714,6 @@ const ImportRepositorySelect = ({ interface RepositoryImportProps { repositories: ReleasesRepository[]; - integrations: Record; versionFilterPrecisionOptions: string[]; onConfirm: (selectedRepositories: ReleasesRepositoryImport[]) => void; isAdmin: boolean; @@ -609,8 +726,6 @@ const RepositoryImportModal = createModal(({ innerProps, const docker = clientApi.docker.getContainers.useQuery(undefined, { refetchOnMount: false, - refetchOnWindowFocus: false, - refetchOnReconnect: false, enabled: innerProps.isAdmin, }); @@ -625,28 +740,25 @@ const RepositoryImportModal = createModal(({ innerProps, if (!identifier) return acc; - const providerKind = containerImageToProviderKind[source] ?? "dockerHub"; - const integrationId = Object.values(innerProps.integrations).find( - (integration) => integration.kind === providerKind, - )?.id; + const provider = containerImageToProviderKind[source] ?? "dockerHub"; + const normalizedIdentifier = normalizeReleaseProviderIdentifier(provider, identifier); - if (acc.some((item) => item.providerIntegrationId === integrationId && item.identifier === identifier)) - return acc; + if (acc.some((item) => item.provider === provider && item.identifier === normalizedIdentifier)) return acc; acc.push({ id: createId(), - providerIntegrationId: integrationId, - identifier, + provider, + identifier: normalizedIdentifier, iconUrl: container.iconUrl ?? undefined, - name: formatIdentifierName(identifier), + name: formatIdentifierName(normalizedIdentifier), versionFilter: version ? parseImageVersionToVersionFilter(version) : undefined, alreadyImported: innerProps.repositories.some( - (item) => item.providerIntegrationId === integrationId && item.identifier === identifier, + (item) => item.provider === provider && item.identifier === normalizedIdentifier, ), }); return acc; }, []) ?? [], - [docker.data, innerProps.repositories, innerProps.integrations], + [docker.data, innerProps.repositories], ); const handleConfirm = useCallback(() => { @@ -722,27 +834,20 @@ const RepositoryImportModal = createModal(({ innerProps, {importRepositories .filter((repository) => !repository.alreadyImported) - .map((repository) => { - const integration = repository.providerIntegrationId - ? innerProps.integrations[repository.providerIntegrationId] - : undefined; - - return ( - - isSelected - ? setSelectedImages([...selectedImages, repository]) - : setSelectedImages(selectedImages.filter((img) => img !== repository)) - } - /> - ); - })} + .map((repository) => ( + + isSelected + ? setSelectedImages([...selectedImages, repository]) + : setSelectedImages(selectedImages.filter((img) => img !== repository)) + } + /> + ))}
)} @@ -756,22 +861,15 @@ const RepositoryImportModal = createModal(({ innerProps, {importRepositories .filter((repository) => repository.alreadyImported) - .map((repository) => { - const integration = repository.providerIntegrationId - ? innerProps.integrations[repository.providerIntegrationId] - : undefined; - - return ( - - ); - })} + .map((repository) => ( + + ))} )} @@ -798,8 +896,8 @@ const RepositoryImportModal = createModal(({ innerProps, size: "xl", }); -const containerImageToProviderKind: Record = { - "ghcr.io": "github", +const containerImageToProviderKind: Record = { + "ghcr.io": "gitHubContainerRegistry", "docker.io": "dockerHub", "lscr.io": "linuxServerIO", "quay.io": "quay", diff --git a/packages/widgets/src/anchor-note/component.tsx b/packages/widgets/src/anchor-note/component.tsx index 99585b838..90a1c998c 100644 --- a/packages/widgets/src/anchor-note/component.tsx +++ b/packages/widgets/src/anchor-note/component.tsx @@ -145,18 +145,10 @@ interface AnchorNoteWidgetContentProps { const AnchorNoteWidgetContent = ({ options, integrationId, noteId }: AnchorNoteWidgetContentProps) => { const t = useScopedI18n("widget.anchorNote"); - const { data: note, refetch } = clientApi.widget.anchorNotes.getNote.useQuery( - { - integrationId, - noteId, - }, - { - staleTime: 15 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); + const { data: note, refetch } = clientApi.widget.anchorNotes.getNote.useQuery({ + integrationId, + noteId, + }); const { mutateAsync: updateNoteAsync, isPending: isUpdating } = clientApi.widget.anchorNotes.updateNote.useMutation(); const [isEditing, setIsEditing] = useState(false); diff --git a/packages/widgets/src/app/app.module.css b/packages/widgets/src/app/app.module.css index e1477f431..b14962396 100644 --- a/packages/widgets/src/app/app.module.css +++ b/packages/widgets/src/app/app.module.css @@ -8,6 +8,8 @@ transition: scale 0.2s ease-in-out; user-select: none; -webkit-touch-callout: none; + -webkit-user-drag: none; + touch-action: manipulation; } .appWithUrl:hover > .appIcon { diff --git a/packages/widgets/src/app/component.tsx b/packages/widgets/src/app/component.tsx index 054a2da6b..56e079a8c 100644 --- a/packages/widgets/src/app/component.tsx +++ b/packages/widgets/src/app/component.tsx @@ -23,16 +23,9 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget const t = useI18n(); const settings = useSettings(); const board = useRequiredBoard(); - const { data: app } = clientApi.app.byId.useQuery( - { - id: options.appId, - }, - { - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); + const { data: app } = clientApi.app.byId.useQuery({ + id: options.appId, + }); useRegisterSpotlightContextResults( `app-${app?.id ?? options.appId}`, app?.href @@ -87,6 +80,7 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget justify="center" align="center" gap={isColumnLayout ? 0 : "sm"} + onContextMenu={isEditMode ? (e) => e.preventDefault() : undefined} > {options.showTitle && ( diff --git a/packages/widgets/src/app/ping/ping-indicator.tsx b/packages/widgets/src/app/ping/ping-indicator.tsx index b8f88cdc8..75dc38974 100644 --- a/packages/widgets/src/app/ping/ping-indicator.tsx +++ b/packages/widgets/src/app/ping/ping-indicator.tsx @@ -11,20 +11,7 @@ interface PingIndicatorProps { export const PingIndicator = ({ appId }: PingIndicatorProps) => { const t = useI18n(); - const { data: pingResult } = clientApi.widget.app.ping.useQuery( - { id: appId }, - { refetchOnMount: false, refetchOnWindowFocus: false }, - ); - - const utils = clientApi.useUtils(); - clientApi.widget.app.updatedPing.useSubscription( - { id: appId }, - { - onData(data) { - utils.widget.app.ping.setData({ id: appId }, data); - }, - }, - ); + const { data: pingResult } = clientApi.widget.app.ping.useQuery({ id: appId }, { refetchOnMount: false }); if (!pingResult) return ; diff --git a/packages/widgets/src/archive-team-warrior/component.tsx b/packages/widgets/src/archive-team-warrior/component.tsx index dacac283d..032858c2f 100644 --- a/packages/widgets/src/archive-team-warrior/component.tsx +++ b/packages/widgets/src/archive-team-warrior/component.tsx @@ -28,20 +28,7 @@ const ArchiveTeamWarriorWidgetContent = ({ integrationId: string; options: WidgetComponentProps<"archiveTeamWarrior">["options"]; }) => { - const [data] = clientApi.widget.archiveTeamWarrior.getStatus.useSuspenseQuery({ - integrationId, - }); - - const utils = clientApi.useUtils(); - - clientApi.widget.archiveTeamWarrior.subscribeStatus.useSubscription( - { integrationId }, - { - onData: (next) => { - utils.widget.archiveTeamWarrior.getStatus.setData({ integrationId }, next); - }, - }, - ); + const [data] = clientApi.widget.archiveTeamWarrior.getStatus.useSuspenseQuery({ integrationId }); const status = data.status; const projectName = status.project?.title ?? status.selectedProject ?? "No project selected"; diff --git a/packages/widgets/src/audio-stats/component.tsx b/packages/widgets/src/audio-stats/component.tsx index d9d8d26d3..9e410f527 100644 --- a/packages/widgets/src/audio-stats/component.tsx +++ b/packages/widgets/src/audio-stats/component.tsx @@ -7,10 +7,9 @@ import type { WidgetComponentProps } from "../definition"; import { AudioStatsContent } from "./audio-stats-content"; export default function AudioStatsWidget({ integrationIds, options, width }: WidgetComponentProps<"audioStats">) { - const { data: response } = clientApi.widget.audioStats.getStats.useQuery( - { integrationId: integrationIds[0] ?? "" }, - { staleTime: 5 * 60 * 1000 }, - ); + const { data: response } = clientApi.widget.audioStats.getStats.useQuery({ + integrationId: integrationIds[0] ?? "", + }); if (!response) return ; diff --git a/packages/widgets/src/bazarr/component.module.css b/packages/widgets/src/bazarr/component.module.css new file mode 100644 index 000000000..9b5857379 --- /dev/null +++ b/packages/widgets/src/bazarr/component.module.css @@ -0,0 +1,77 @@ +.root { + container-type: size; + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + padding: clamp(6px, 3cqi, 14px); + gap: clamp(4px, 2cqi, 10px); + overflow: hidden; +} + +.grid { + display: grid; + flex: 1; + min-height: 0; + gap: clamp(4px, 2cqi, 10px); + grid-template-columns: repeat(var(--stat-cols, 2), minmax(0, 1fr)); + grid-auto-rows: 1fr; + align-content: stretch; +} + +.statTile { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 0; + min-width: 0; + gap: clamp(2px, 1.5cqi, 5px); + padding: clamp(4px, 2.5cqi, 10px); + border-radius: var(--mantine-radius-md); + background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6)); + overflow: hidden; +} + +.statTileWarning { + background-color: light-dark(var(--mantine-color-orange-0), var(--mantine-color-dark-5)); +} + +.statIcon { + color: var(--mantine-color-dimmed); + flex-shrink: 0; +} + +.statLabel { + font-size: clamp(8px, 2.5cqi, 12px); + color: var(--mantine-color-dimmed); + line-height: 1.2; + text-align: center; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.statValue { + font-size: clamp(14px, 5.5cqi, 24px); + font-weight: 700; + color: var(--mantine-primary-color-filled); + line-height: 1.1; + text-align: center; +} + +.statValueWarning { + color: var(--mantine-color-orange-6); +} + +.emptyState { + display: flex; + flex: 1; + align-items: center; + justify-content: center; + min-height: 0; + color: var(--mantine-color-dimmed); + font-size: clamp(11px, 3cqi, 14px); + text-align: center; +} diff --git a/packages/widgets/src/bazarr/component.tsx b/packages/widgets/src/bazarr/component.tsx new file mode 100644 index 000000000..094c32ee1 --- /dev/null +++ b/packages/widgets/src/bazarr/component.tsx @@ -0,0 +1,103 @@ +"use client"; + +import type { CSSProperties } from "react"; +import { Text } from "@mantine/core"; +import { IconAlertTriangle, IconDeviceTv, IconMovie, IconPlugConnected } from "@tabler/icons-react"; + +import { clientApi } from "@homarr/api/client"; +import { useScopedI18n } from "@homarr/translation/client"; + +import { WidgetEmptyState } from "../common/empty-state"; +import type { WidgetComponentProps } from "../definition"; +import classes from "./component.module.css"; + +const statVisibilityByOption = { + showMissingEpisodes: "episodes", + showMissingMovies: "movies", + showProviderIssues: "providers", + showHealthIssues: "status", +} as const; + +const statIcons = { + episodes: IconDeviceTv, + movies: IconMovie, + providers: IconPlugConnected, + status: IconAlertTriangle, +} as const; + +const gridColsByWidth = [ + { minWidth: 380, cols: 2 }, + { minWidth: 0, cols: 1 }, +] as const; + +const iconSizeByWidth = [ + { minWidth: 320, size: 22 }, + { minWidth: 200, size: 18 }, + { minWidth: 0, size: 16 }, +] as const; + +export default function BazarrWidget({ integrationIds, options, width }: WidgetComponentProps<"bazarr">) { + const t = useScopedI18n("widget.bazarr"); + const { data: badges } = clientApi.widget.bazarr.getBadges.useQuery( + { integrationId: integrationIds[0] ?? "" }, + { enabled: Boolean(integrationIds[0]) }, + ); + + if (!badges) return ; + + const statValues = { + episodes: badges.episodes, + movies: badges.movies, + providers: badges.providers, + status: badges.status, + } as const; + + const visibleStatKeys = Object.entries(statVisibilityByOption) + .filter(([optionKey]) => options[optionKey as keyof typeof options]) + .map(([, statKey]) => statKey); + + const gridCols = getGridCols(width); + const iconSize = getIconSize(width); + + if (visibleStatKeys.length === 0) { + return ( +
+
+ + — + +
+
+ ); + } + + return ( +
+
+ {visibleStatKeys.map((statKey) => { + const Icon = statIcons[statKey]; + const value = statValues[statKey]; + const isWarning = (statKey === "providers" || statKey === "status") && value > 0; + + return ( +
+ + {value} + {t(statKey)} +
+ ); + })} +
+
+ ); +} + +function getGridCols(width: number): number { + const match = gridColsByWidth.find(({ minWidth }) => width >= minWidth); + return match?.cols ?? 1; +} + +function getIconSize(width: number): number { + const match = iconSizeByWidth.find(({ minWidth }) => width >= minWidth); + return match?.size ?? 16; +} diff --git a/packages/widgets/src/bazarr/index.ts b/packages/widgets/src/bazarr/index.ts new file mode 100644 index 000000000..f2e79f570 --- /dev/null +++ b/packages/widgets/src/bazarr/index.ts @@ -0,0 +1,36 @@ +import { IconBadgeCc, IconServerOff } from "@tabler/icons-react"; + +import { createWidgetDefinition } from "../definition"; +import { optionsBuilder } from "../options"; + +export const { definition, componentLoader } = createWidgetDefinition("bazarr", { + icon: IconBadgeCc, + supportedIntegrations: ["bazarr"], + integrationsRequired: true, + createOptions() { + return optionsBuilder.from((factory) => ({ + showMissingEpisodes: factory.switch({ + defaultValue: true, + withDescription: true, + }), + showMissingMovies: factory.switch({ + defaultValue: true, + withDescription: true, + }), + showProviderIssues: factory.switch({ + defaultValue: true, + withDescription: true, + }), + showHealthIssues: factory.switch({ + defaultValue: true, + withDescription: true, + }), + })); + }, + errors: { + INTERNAL_SERVER_ERROR: { + icon: IconServerOff, + message: (t) => t("widget.bazarr.error.internalServerError"), + }, + }, +}).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/beszel-alerts/component.tsx b/packages/widgets/src/beszel-alerts/component.tsx index 7c2bcc06c..3f934423d 100644 --- a/packages/widgets/src/beszel-alerts/component.tsx +++ b/packages/widgets/src/beszel-alerts/component.tsx @@ -41,36 +41,7 @@ export default function BeszelAlertsWidget({ () => ({ integrationIds, includeHistory: options.showHistory, maxHistoryItems: options.maxHistoryItems }), [integrationIds, options.showHistory, options.maxHistoryItems], ); - const { - data: results = [], - error: alertsError, - isPending, - } = clientApi.widget.beszel.getAlerts.useQuery(alertsInput, { - staleTime: 10_000, - gcTime: 48 * 60 * 60 * 1000, - refetchInterval: 10_000, - retry: false, - }); - const utils = clientApi.useUtils(); - - clientApi.widget.beszel.subscribeAlerts.useSubscription(alertsInput, { - enabled: !isEditMode, - onData(data) { - utils.widget.beszel.getAlerts.setData(alertsInput, (prev) => { - if (!prev) return prev; - return prev.map((r) => - r.integrationId === data.integrationId - ? { - ...r, - alerts: data.alerts.alerts, - history: data.alerts.history, - updatedAt: data.timestamp, - } - : r, - ); - }); - }, - }); + const { data: results = [], error: alertsError, isPending } = clientApi.widget.beszel.getAlerts.useQuery(alertsInput); const systemNameMap = useMemo(() => { const map: Record = {}; diff --git a/packages/widgets/src/beszel-system-grid/component.tsx b/packages/widgets/src/beszel-system-grid/component.tsx index 3d5c3bd42..51e5d014c 100644 --- a/packages/widgets/src/beszel-system-grid/component.tsx +++ b/packages/widgets/src/beszel-system-grid/component.tsx @@ -27,10 +27,18 @@ import classes from "./component.module.css"; import type { WidgetComponentProps } from "../definition"; import type { BeszelSystemRow } from "../beszel/_shared/types"; import { statusColorMap, thresholdColor } from "../beszel/_shared/colors"; -import { formatByteRate, formatLoadAvg, formatPercent, formatTemp, formatUptime } from "../beszel/_shared/format"; -import { useBeszelFilteredSystems, useBeszelSystemsSubscription } from "../beszel/_shared/hooks"; +import { + formatByteRate, + formatLoadAvg, + formatPercent, + formatTemp, + formatUptime, + getProgressTrackSize, +} from "../beszel/_shared/format"; +import { useBeszelFilteredSystems } from "../beszel/_shared/hooks"; import { BeszelIntegrationErrorIndicator } from "../beszel/_shared/error-indicator"; import { BeszelSystemStatsModal } from "../beszel/_shared/system-stats-modal"; +import { DiskUsage } from "../beszel/_shared/disk-usage"; interface SizeConfig { iconSize: number; @@ -106,22 +114,14 @@ const getMaxVisibleMetrics = (cellHeight: number, size: SizeConfig): number => { return Math.max(1, Math.floor(available / size.rowHeight)); }; -const MIN_CELL_WIDTH = 120; +const MIN_CELL_WIDTH = 180; const MIN_CELL_HEIGHT = 80; -const getColCount = (width: number, itemCount: number): number => { +const getColCount = (width: number, height: number, itemCount: number): number => { if (itemCount <= 1) return 1; const maxCols = Math.min(itemCount, Math.max(1, Math.floor(width / MIN_CELL_WIDTH))); - - let best = 1; - for (let c = 1; c <= maxCols; c++) { - const emptyCells = (c - (itemCount % c)) % c; - const bestEmpty = (best - (itemCount % best)) % best; - if (emptyCells < bestEmpty || (emptyCells === bestEmpty && c > best)) { - best = c; - } - } - return best; + const aspectRatio = width / Math.max(height, MIN_CELL_HEIGHT); + return Math.min(maxCols, Math.max(1, Math.ceil(Math.sqrt(itemCount * aspectRatio)))); }; interface MetricRowProps { @@ -133,20 +133,25 @@ interface MetricRowProps { } const MetricValue = ({ value, progress, size }: Pick) => ( - - + + {value} {progress && ( - + )} ); const MetricRow = ({ icon, label, value, progress, size }: MetricRowProps) => ( - + {icon} - + {label} @@ -195,14 +200,13 @@ const metricRenderers = [ { key: "showDisk", render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => ( - } - label={t("metric.disk")} - value={formatPercent(s.disk)} - progress={{ value: s.disk, color: thresholdColor(s.disk) }} - size={sz} - /> + + + + {t("metric.disk")} + + + ), visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showDisk, }, @@ -369,12 +373,8 @@ export default function BeszelSystemGridWidget({ data: results = [], error: systemsError, isPending, - } = clientApi.widget.beszel.getSystems.useQuery( - { integrationIds }, - { staleTime: 10_000, gcTime: 48 * 60 * 60 * 1000, refetchInterval: 10_000, retry: false }, - ); + } = clientApi.widget.beszel.getSystems.useQuery({ integrationIds }); - useBeszelSystemsSubscription(integrationIds, !isEditMode); const filteredSystems = useBeszelFilteredSystems(results, options.statusFilter); if (systemsError) throw systemsError; @@ -403,7 +403,7 @@ export default function BeszelSystemGridWidget({ ); } - const cols = getColCount(width, filteredSystems.length); + const cols = getColCount(width, height, filteredSystems.length); const rows = Math.ceil(filteredSystems.length / cols) || 1; const rawCellHeight = height / rows; const scrollEnabled = rawCellHeight < MIN_CELL_HEIGHT; diff --git a/packages/widgets/src/beszel-system-grid/index.ts b/packages/widgets/src/beszel-system-grid/index.ts index 9551adfc6..69dc7e2ba 100644 --- a/packages/widgets/src/beszel-system-grid/index.ts +++ b/packages/widgets/src/beszel-system-grid/index.ts @@ -6,6 +6,7 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("beszelSystemGrid", { icon: IconLayoutGrid, + queryKey: [["widget", "beszel"]], supportedIntegrations: ["beszel", "mock"], integrationsRequired: true, createOptions() { diff --git a/packages/widgets/src/beszel-system-stats/component.tsx b/packages/widgets/src/beszel-system-stats/component.tsx index 02d77a832..91dcd7292 100644 --- a/packages/widgets/src/beszel-system-stats/component.tsx +++ b/packages/widgets/src/beszel-system-stats/component.tsx @@ -1,53 +1,21 @@ "use client"; import { useCallback, useMemo } from "react"; -import { Button, Box, Center, Loader, Menu, ScrollArea, Select, SimpleGrid, Stack, Text } from "@mantine/core"; -import { IconPlugConnectedX, IconServer, IconQuestionMark, IconServerOff } from "@tabler/icons-react"; +import { Box, Button, Center, Loader, Menu, ScrollArea, Select, Stack, Text } from "@mantine/core"; +import { IconQuestionMark, IconServer, IconServerOff } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; import { useSession } from "@homarr/auth/client"; import { constructBoardPermissions } from "@homarr/auth/shared"; import { useOptionalBoard } from "@homarr/boards/context"; -import type { BeszelContainerStatsRecord, BeszelSystemStatsRecord } from "@homarr/integrations/types"; import { useScopedI18n } from "@homarr/translation/client"; import classes from "./component.module.css"; import type { WidgetComponentProps } from "../definition"; -import { containerColors } from "../beszel/_shared/colors"; import type { BeszelTimePeriod } from "../beszel/_shared/chart"; -import { - BeszelChartPanel, - CPU_Y_AXIS_DOMAIN, - useContainerNames, - useDockerChartData, - useSystemChartData, -} from "../beszel/_shared/chart"; -import { - chartAxisFormatters, - formatByteRate, - formatGB, - formatPercent, - formatStorageBytes, -} from "../beszel/_shared/format"; import { BeszelIntegrationErrorIndicator } from "../beszel/_shared/error-indicator"; -import { makeTooltipProps } from "../beszel/_shared/tooltip"; -import { useLiveStats } from "../beszel/_shared/use-live-stats"; - -const tooltipPercent = makeTooltipProps(formatPercent); -const tooltipGB = makeTooltipProps(formatGB, true); -const tooltipRate = makeTooltipProps(formatByteRate, true); -const tooltipPercentTotal = makeTooltipProps(formatPercent, true); -const tooltipBytesTotal = makeTooltipProps(formatStorageBytes, true); - -const CHART_HEIGHT = 180; -const MB = 1024 * 1024; -const gridColumns = [1, 2] as const; - -function statsWhenShown(show: boolean | undefined, data: T | undefined): T | undefined { - if (!show || !data) return undefined; - return data; -} +import { BeszelStatsView } from "../beszel/_shared/stats-view"; export default function BeszelSystemStatsWidget({ options, @@ -67,20 +35,15 @@ export default function BeszelSystemStatsWidget({ data: systemsResult = [], isPending: systemsPending, error: systemsError, - } = clientApi.widget.beszel.getSystems.useQuery( - { integrationIds }, - { staleTime: 10_000, gcTime: 48 * 60 * 60 * 1000, retry: false }, - ); + } = clientApi.widget.beszel.getSystems.useQuery({ integrationIds }); const systems = useMemo( - () => systemsResult.flatMap((r) => r.systems.map((s) => ({ value: s.id, label: s.name }))), + () => systemsResult.flatMap((result) => result.systems.map((system) => ({ value: system.id, label: system.name }))), [systemsResult], ); - const selectedSystem = options.systemId || systems[0]?.value || ""; - const selectedLabel = systems.find((s) => s.value === selectedSystem)?.label; - const systemExists = systems.some((s) => s.value === selectedSystem); - const systemReady = !systemsPending && (selectedSystem === "" || systemExists); + const selectedLabel = systems.find((system) => system.value === selectedSystem)?.label; + const systemExists = systems.some((system) => system.value === selectedSystem); const handleSelectSystem = useCallback( (value: string) => { @@ -92,122 +55,13 @@ export default function BeszelSystemStatsWidget({ [setOptions, hasChangeAccess, boardId, itemId, saveItemOptions], ); - const showDocker = options.showDockerCpu || options.showDockerMemory || options.showDockerNetwork; - const isLive = options.timePeriod === "1m"; - - const { data: statsResult, error: statsError } = clientApi.widget.beszel.getSystemStats.useQuery( - { - integrationIds, - systemId: selectedSystem, - timePeriod: options.timePeriod as "1m" | "1h" | "12h" | "24h" | "1w" | "30d", - includeDocker: showDocker, - }, - { - staleTime: 10_000, - gcTime: 48 * 60 * 60 * 1000, - refetchInterval: isLive ? false : 10_000, - enabled: !isLive && systemReady && selectedSystem !== "", - retry: false, - }, - ); - - const { data: liveData, error: liveError } = useLiveStats(integrationIds, selectedSystem, isLive && systemReady); - - let activeStats: - | { systemStats: BeszelSystemStatsRecord[]; containerStats: BeszelContainerStatsRecord[]; error?: string } - | null - | undefined = statsResult; - if (isLive) { - activeStats = liveData; - } - - const mappers = useMemo( - () => ({ - cpu: (s: { cpu: number }) => ({ [t("chart.cpu.series")]: s.cpu }), - memory: (s: { mu: number; mb: number }) => ({ - [t("chart.memory.series")]: s.mu, - [t("chart.memory.cache")]: s.mb ?? 0, - }), - disk: (s: { du: number }) => ({ [t("chart.disk.series")]: s.du }), - diskIO: (s: { dr?: number; dw?: number }) => ({ - [t("chart.diskIO.read")]: (s.dr ?? 0) * MB, - [t("chart.diskIO.write")]: (s.dw ?? 0) * MB, - }), - network: (s: { ns: number; nr: number; b?: [number, number] }) => ({ - [t("chart.network.sent")]: s.b?.[0] ?? s.ns, - [t("chart.network.recv")]: s.b?.[1] ?? s.nr, - }), - }), - [t], - ); - - const series = useMemo( - () => ({ - cpu: [{ name: t("chart.cpu.series"), color: "teal.6" }], - memory: [ - { name: t("chart.memory.series"), color: "teal.5" }, - { name: t("chart.memory.cache"), color: "teal.8" }, - ], - disk: [{ name: t("chart.disk.series"), color: "grape.6" }], - diskIO: [ - { name: t("chart.diskIO.write"), color: "orange.6" }, - { name: t("chart.diskIO.read"), color: "blue.6" }, - ], - network: [ - { name: t("chart.network.sent"), color: "blue.6" }, - { name: t("chart.network.recv"), color: "teal.6" }, - ], - }), - [t], - ); - - const systemStats = activeStats?.systemStats; - const containerStatsRaw = activeStats?.containerStats; - - const timePeriod = options.timePeriod as BeszelTimePeriod; - const cpuData = useSystemChartData(statsWhenShown(options.showCpu, systemStats), mappers.cpu, timePeriod); - const memoryData = useSystemChartData(statsWhenShown(options.showMemory, systemStats), mappers.memory, timePeriod); - const diskData = useSystemChartData(statsWhenShown(options.showDisk, systemStats), mappers.disk, timePeriod); - const diskIOData = useSystemChartData(statsWhenShown(options.showDiskIO, systemStats), mappers.diskIO, timePeriod); - const networkData = useSystemChartData(statsWhenShown(options.showNetwork, systemStats), mappers.network, timePeriod); - - const containerNames = useContainerNames(statsWhenShown(showDocker, containerStatsRaw)); - const dockerCpuData = useDockerChartData( - statsWhenShown(options.showDockerCpu, containerStatsRaw), - containerNames, - "cpu", - timePeriod, - ); - const dockerMemData = useDockerChartData( - statsWhenShown(options.showDockerMemory, containerStatsRaw), - containerNames, - "memory", - timePeriod, - ); - const dockerNetData = useDockerChartData( - statsWhenShown(options.showDockerNetwork, containerStatsRaw), - containerNames, - "network", - timePeriod, - ); - - const containerSeries = useMemo( - () => containerNames.map((name, i) => ({ name, color: containerColors[i % containerColors.length] as string })), - [containerNames], - ); - - const cols = gridColumns[Number(width > 600)]; - if (systemsError) throw systemsError; - if (!isLive && statsError) throw statsError; - - if (systemsPending) { + if (systemsPending) return (
); - } if (systems.length === 0) { return ( @@ -225,7 +79,7 @@ export default function BeszelSystemStatsWidget({ ); } - if (!systemsPending && !systemExists && systems.length > 0) { + if (!systemExists) { return (
@@ -245,22 +99,6 @@ export default function BeszelSystemStatsWidget({ ); } - if (isLive && liveError && !liveData) { - return ( -
- - - - {t("error.liveConnectionFailed")} - - - -
- ); - } - return ( @@ -269,7 +107,7 @@ export default function BeszelSystemStatsWidget({ {!isEditMode && systems.length > 1 && ( @@ -285,161 +123,37 @@ export default function BeszelSystemStatsWidget({ - {systems.map((s) => ( + {systems.map((system) => ( handleSelectSystem(s.value)} + fw={system.value === selectedSystem ? 600 : 400} + c={system.value !== selectedSystem ? "dimmed" : undefined} + onClick={() => handleSelectSystem(system.value)} > - {s.label} + {system.label} ))} )} - - {!isLive && !activeStats && systemReady && selectedSystem !== "" && ( -
- -
- )} - - {!isLive && activeStats && "error" in activeStats && activeStats.error && ( -
- - - - {activeStats.error} - - -
- )} - - {activeStats && (!("error" in activeStats) || !activeStats.error) && ( - - {options.showCpu && cpuData.length > 0 && ( - - )} - - {options.showMemory && memoryData.length > 0 && ( - - )} - - {options.showDisk && diskData.length > 0 && ( - - )} - - {options.showDiskIO && diskIOData.length > 0 && ( - - )} - - {options.showNetwork && networkData.length > 0 && ( - - )} - - {showDocker && containerSeries.length > 0 && ( - <> - {options.showDockerCpu && dockerCpuData.length > 0 && ( - - )} - - {options.showDockerMemory && dockerMemData.length > 0 && ( - - )} - - {options.showDockerNetwork && dockerNetData.length > 0 && ( - - )} - - )} - - )} + 600 ? 2 : 1} + visibility={{ + cpu: options.showCpu, + memory: options.showMemory, + disk: options.showDisk, + diskIO: options.showDiskIO, + network: options.showNetwork, + dockerCpu: options.showDockerCpu, + dockerMemory: options.showDockerMemory, + dockerNetwork: options.showDockerNetwork, + }} + onSwitchToHistorical={() => setOptions({ newOptions: { timePeriod: "1h" } })} + />
diff --git a/packages/widgets/src/beszel-system-stats/index.ts b/packages/widgets/src/beszel-system-stats/index.ts index 0b1cdf127..af2907b25 100644 --- a/packages/widgets/src/beszel-system-stats/index.ts +++ b/packages/widgets/src/beszel-system-stats/index.ts @@ -16,6 +16,7 @@ const timePeriodOptions = [ export const { definition, componentLoader } = createWidgetDefinition("beszelSystemStats", { icon: IconChartAreaLine, + queryKey: [["widget", "beszel"]], supportedIntegrations: ["beszel", "mock"], integrationsRequired: true, createOptions() { @@ -28,10 +29,7 @@ export const { definition, componentLoader } = createWidgetDefinition("beszelSys data = [], isPending, isError, - } = clientApi.widget.beszel.getSystems.useQuery( - { integrationIds }, - { enabled: integrationIds.length > 0, staleTime: 30_000 }, - ); + } = clientApi.widget.beszel.getSystems.useQuery({ integrationIds }, { enabled: integrationIds.length > 0 }); const selectData = data.flatMap((r) => r.systems.map((s) => ({ value: s.id, label: s.name }))); return { data: selectData, isPending, isError }; }, diff --git a/packages/widgets/src/beszel-system-table/component.tsx b/packages/widgets/src/beszel-system-table/component.tsx index 1d8c92355..5352eb170 100644 --- a/packages/widgets/src/beszel-system-table/component.tsx +++ b/packages/widgets/src/beszel-system-table/component.tsx @@ -26,10 +26,18 @@ import { useScopedI18n } from "@homarr/translation/client"; import type { WidgetComponentProps } from "../definition"; import type { BeszelSystemRow } from "../beszel/_shared/types"; import { loadAvgColor, statusColorMap, thresholdColor } from "../beszel/_shared/colors"; -import { formatByteRate, formatLoadAvg, formatPercent, formatTemp, formatUptime } from "../beszel/_shared/format"; -import { useBeszelFilteredSystems, useBeszelSystemsSubscription } from "../beszel/_shared/hooks"; +import { + formatByteRate, + formatLoadAvg, + formatPercent, + formatTemp, + formatUptime, + getProgressTrackSize, +} from "../beszel/_shared/format"; +import { useBeszelFilteredSystems } from "../beszel/_shared/hooks"; import { BeszelIntegrationErrorIndicator } from "../beszel/_shared/error-indicator"; import { BeszelSystemStatsModal } from "../beszel/_shared/system-stats-modal"; +import { DiskUsage } from "../beszel/_shared/disk-usage"; const directionMultiplier: Record = { asc: 1, desc: -1 }; @@ -74,13 +82,9 @@ export default function BeszelSystemTableWidget({ data: results = [], error: systemsError, isPending, - } = clientApi.widget.beszel.getSystems.useQuery( - { integrationIds }, - { staleTime: 10_000, gcTime: 48 * 60 * 60 * 1000, refetchInterval: 10_000, retry: false }, - ); + } = clientApi.widget.beszel.getSystems.useQuery({ integrationIds }); const size = getSizeConfig(width); - useBeszelSystemsSubscription(integrationIds, !isEditMode); const filteredSystems = useBeszelFilteredSystems(results, options.statusFilter); const [sortStatus, setSortStatus] = useState>({ @@ -101,11 +105,16 @@ export default function BeszelSystemTableWidget({ }, [filteredSystems, sortStatus]); const PercentCell = ({ value }: { value: number }) => ( - - + + {formatPercent(value)} - + ); @@ -160,7 +169,15 @@ export default function BeszelSystemTableWidget({ ), sortable: true, - render: (record) => , + render: (record) => ( + + ), }, options.showGpu && { accessor: "gpu", diff --git a/packages/widgets/src/beszel-system-table/index.ts b/packages/widgets/src/beszel-system-table/index.ts index 538649afd..ba90cb5e9 100644 --- a/packages/widgets/src/beszel-system-table/index.ts +++ b/packages/widgets/src/beszel-system-table/index.ts @@ -25,6 +25,7 @@ const sortDirectionOptions = [ export const { definition, componentLoader } = createWidgetDefinition("beszelSystemTable", { icon: IconTable, + queryKey: [["widget", "beszel"]], supportedIntegrations: ["beszel", "mock"], integrationsRequired: true, createOptions() { diff --git a/packages/widgets/src/beszel/_shared/chart.spec.ts b/packages/widgets/src/beszel/_shared/chart.spec.ts new file mode 100644 index 000000000..5a86bf2bb --- /dev/null +++ b/packages/widgets/src/beszel/_shared/chart.spec.ts @@ -0,0 +1,87 @@ +import { describe, expect, test } from "vitest"; + +import type { BeszelSystemStatsRecord } from "@homarr/integrations/types"; + +import { buildDiskChartData, padLiveTimeGrid } from "./chart"; + +const record = ( + created: string, + du: number, + efs: BeszelSystemStatsRecord["stats"]["efs"], +): BeszelSystemStatsRecord => ({ + id: created, + system: "system-1", + type: "1m", + created, + updated: created, + stats: { + cpu: 0, + m: 0, + mu: 0, + mp: 0, + mb: 0, + s: 0, + su: 0, + d: 938.8, + du, + dp: 51.15, + efs, + }, +}); + +describe("buildDiskChartData", () => { + test("keeps root and extra filesystem usage in GiB without stacking or byte conversion", () => { + const data = buildDiskChartData( + [ + record("2026-07-11T13:46:00.000Z", 455.81, { + sda: { d: 1906.8, du: 1656.45, r: 0, w: 0 }, + sdb: { d: 5587.04, du: 3936.86, r: 0, w: 0 }, + sdc: { d: 3724.18, du: 1589.84, r: 0, w: 0 }, + }), + ], + ["sda", "sdb", "sdc"], + "Root", + "1h", + ); + + expect(data).toHaveLength(1); + expect(data[0]).toMatchObject({ Root: 455.81, sda: 1656.45, sdb: 3936.86, sdc: 1589.84 }); + }); + + test("uses zero for a filesystem missing from an individual sample and orders historical records oldest first", () => { + const data = buildDiskChartData( + [ + record("2026-07-11T13:47:00.000Z", 2, { sda: { d: 10, du: 4, r: 0, w: 0 } }), + record("2026-07-11T13:46:00.000Z", 1, undefined), + ], + ["sda"], + "Root", + "1h", + ); + + expect(data.map((point) => ({ Root: point.Root, sda: point.sda }))).toEqual([ + { Root: 1, sda: 0 }, + { Root: 2, sda: 4 }, + ]); + }); +}); + +describe("padLiveTimeGrid", () => { + test("keeps a fixed 60-second window with the newest sample at the right edge", () => { + const data = padLiveTimeGrid([{ time: "13:51:30", rawTime: "2026-07-11T13:51:30.217Z", CPU: 18.07 }]); + + expect(data).toHaveLength(60); + expect(data[0]).not.toHaveProperty("CPU"); + expect(data[58]).not.toHaveProperty("CPU"); + expect(data[59]).toMatchObject({ CPU: 18.07 }); + }); + + test("leaves null gaps for missing seconds instead of stretching existing points", () => { + const data = padLiveTimeGrid([ + { time: "13:51:28", rawTime: "2026-07-11T13:51:28.000Z", CPU: 10 }, + { time: "13:51:30", rawTime: "2026-07-11T13:51:30.000Z", CPU: 20 }, + ]); + + expect(data.slice(-3).map((point) => point.CPU)).toEqual([10, undefined, 20]); + }); +}); diff --git a/packages/widgets/src/beszel/_shared/chart.tsx b/packages/widgets/src/beszel/_shared/chart.tsx index 8d8a5e319..7ba5b66fe 100644 --- a/packages/widgets/src/beszel/_shared/chart.tsx +++ b/packages/widgets/src/beszel/_shared/chart.tsx @@ -30,6 +30,7 @@ function prepareRecords(records: T[], timePeriod: BeszelTimePeriod) { } function padTimeGrid(data: Record[], timePeriod: BeszelTimePeriod) { + if (timePeriod === "1m") return padLiveTimeGrid(data); const days = periodDays[timePeriod]; if (!days) return data; @@ -50,6 +51,38 @@ function padTimeGrid(data: Record[], timePeriod: BeszelTimePeri return result.toSorted((a, b) => new Date(a.rawTime as string).getTime() - new Date(b.rawTime as string).getTime()); } +export function padLiveTimeGrid(data: Record[], pointCount = 60) { + if (data.length === 0) return data; + + const end = dayjs( + data.reduce((latest, point) => { + const timestamp = new Date(point.rawTime as string).getTime(); + return Number.isFinite(timestamp) ? Math.max(latest, timestamp) : latest; + }, 0), + ).startOf("second"); + const pointsBySecond = new Map( + data.map( + (point) => + [ + dayjs(point.rawTime as string) + .startOf("second") + .valueOf(), + point, + ] as const, + ), + ); + + return Array.from({ length: pointCount }, (_, index) => { + const timestamp = end.subtract(pointCount - index - 1, "second"); + return ( + pointsBySecond.get(timestamp.valueOf()) ?? { + time: timestamp.format(timeFormats["1m"]), + rawTime: timestamp.toISOString(), + } + ); + }); +} + const yAxisBase = { tickMargin: 0, tick: { fontSize: 10 } } as const; const chartStyle = { minWidth: 0, minHeight: 1 } as const; const panelStyle = { minWidth: 0, overflow: "hidden" } as const; @@ -174,6 +207,40 @@ const defaultContainerExtractors: Record = { network: (c) => (c?.b ? c.b[0] + c.b[1] : (c?.ns ?? 0) + (c?.nr ?? 0)), }; +export const useDiskChartData = ( + systemStats: BeszelSystemStatsRecord[] | undefined, + efsPaths: string[], + rootSeriesName: string, + timePeriod: BeszelTimePeriod = "1h", +) => + useMemo( + () => buildDiskChartData(systemStats, efsPaths, rootSeriesName, timePeriod), + [systemStats, efsPaths, rootSeriesName, timePeriod], + ); + +export const buildDiskChartData = ( + systemStats: BeszelSystemStatsRecord[] | undefined, + efsPaths: string[], + rootSeriesName: string, + timePeriod: BeszelTimePeriod = "1h", +) => { + if (!systemStats?.length) return []; + const { fmt, ordered } = prepareRecords(systemStats, timePeriod); + const mapped = ordered.map((record) => { + const point: Record = { + time: fmt(record.created), + rawTime: record.created, + [rootSeriesName]: record.stats.du, + }; + const efs = record.stats.efs ?? {}; + for (const path of efsPaths) { + point[path] = efs[path]?.du ?? 0; + } + return point; + }); + return padTimeGrid(mapped, timePeriod); +}; + export const useDockerChartData = ( containerStats: BeszelContainerStatsRecord[] | undefined, containerNames: string[], diff --git a/packages/widgets/src/beszel/_shared/disk-usage.tsx b/packages/widgets/src/beszel/_shared/disk-usage.tsx new file mode 100644 index 000000000..9aa2a14ad --- /dev/null +++ b/packages/widgets/src/beszel/_shared/disk-usage.tsx @@ -0,0 +1,105 @@ +import { Box, Group, HoverCard, Progress, Stack, Text, UnstyledButton } from "@mantine/core"; + +import type { BeszelSystemRow } from "./types"; +import { thresholdColor } from "./colors"; +import { formatPercent, getProgressTrackSize } from "./format"; + +interface DiskUsageProps { + system: BeszelSystemRow; + fontSize: string; + progressSize: "xs" | "sm"; + valueMiw: number; + valueGap?: number; +} + +const mountLabel = (path: string) => + path === "/" ? "ROOT" : (path.split("/").filter(Boolean).at(-1) ?? path).toUpperCase(); +const severityColor = (value: number) => `var(--mantine-color-${thresholdColor(value)}-6)`; + +export const DiskUsage = ({ system, fontSize, progressSize, valueMiw, valueGap = 6 }: DiskUsageProps) => { + const filesystems = Object.entries(system.extraFilesystems).filter(([path]) => path !== "/"); + const trackSize = getProgressTrackSize(progressSize); + const dotSize = progressSize === "xs" ? 2 : 3; + const dotGap = 2; + + return ( + + + {formatPercent(system.disk)} + + + + {filesystems.length > 0 ? ( + + + + + {filesystems.map(([path, value]) => ( + + ))} + + + + ) : ( + + + + + + )} + + + + {[["/", system.disk] as const, ...filesystems].map(([path, value]) => ( + + + {mountLabel(path)} + + + + {formatPercent(value)} + + + + + ))} + + + + + ); +}; diff --git a/packages/widgets/src/beszel/_shared/format.spec.ts b/packages/widgets/src/beszel/_shared/format.spec.ts new file mode 100644 index 000000000..ec4545063 --- /dev/null +++ b/packages/widgets/src/beszel/_shared/format.spec.ts @@ -0,0 +1,21 @@ +import { describe, expect, test } from "vitest"; + +import { chartAxisFormatters, formatGB, getProgressTrackSize } from "./format"; + +describe("Beszel storage formatting", () => { + test("keeps smaller values in GB", () => { + expect(formatGB(455.81)).toBe("455.81 GB"); + expect(chartAxisFormatters.gb(455.81)).toBe("456G"); + }); + + test("promotes large GiB values to TB", () => { + expect(formatGB(3323)).toBe("3.25 TB"); + expect(formatGB(3936.86)).toBe("3.84 TB"); + expect(chartAxisFormatters.gb(3323)).toBe("3.2T"); + }); + + test("maps progress sizes consistently", () => { + expect(getProgressTrackSize("xs")).toBe(6); + expect(getProgressTrackSize("sm")).toBe(9); + }); +}); diff --git a/packages/widgets/src/beszel/_shared/format.ts b/packages/widgets/src/beszel/_shared/format.ts index 8e84c74e4..dc483daa6 100644 --- a/packages/widgets/src/beszel/_shared/format.ts +++ b/packages/widgets/src/beszel/_shared/format.ts @@ -29,14 +29,22 @@ export const formatStorageBytes = (bytes: number): string => formatScaled(bytes, export const formatGB = (value: number): string => { if (!value || !Number.isFinite(value)) return "0 GB"; + if (Math.abs(value) >= 1024) return `${(value / 1024).toFixed(2)} TB`; return `${value.toFixed(2)} GB`; }; export const formatPercent = (value: number): string => `${value.toFixed(1)}%`; +export const getProgressTrackSize = (size: "xs" | "sm"): number => (size === "xs" ? 6 : 9); + export const chartAxisFormatters = { percent: formatPercent, - gb: (value: number) => (!value ? "0" : `${Number(value).toFixed(0)}G`), + gb: (value: number) => { + const numericValue = Number(value); + if (!numericValue) return "0"; + if (Math.abs(numericValue) >= 1024) return `${(numericValue / 1024).toFixed(1)}T`; + return `${numericValue.toFixed(0)}G`; + }, mb: (value: number) => (!value ? "0" : `${Number(value).toFixed(0)}M`), bytes: (value: number) => formatScaledCompact(Number(value), byteUnits, "0"), rate: (value: number) => formatScaledCompact(Number(value), rateUnits, "0"), diff --git a/packages/widgets/src/beszel/_shared/hooks.ts b/packages/widgets/src/beszel/_shared/hooks.ts index ebb84b4eb..b00e6f857 100644 --- a/packages/widgets/src/beszel/_shared/hooks.ts +++ b/packages/widgets/src/beszel/_shared/hooks.ts @@ -2,30 +2,10 @@ import { useMemo } from "react"; -import { clientApi } from "@homarr/api/client"; - import type { BeszelSystemRow } from "./types"; type SystemRowWithKey = BeszelSystemRow & { _key: string }; -export const useBeszelSystemsSubscription = (integrationIds: string[], enabled = true) => { - const utils = clientApi.useUtils(); - clientApi.widget.beszel.subscribeSystems.useSubscription( - { integrationIds }, - { - enabled, - onData(data) { - utils.widget.beszel.getSystems.setData({ integrationIds }, (prev) => { - if (!prev) return prev; - return prev.map((r) => - r.integrationId === data.integrationId ? { ...r, systems: data.systems, updatedAt: data.timestamp } : r, - ); - }); - }, - }, - ); -}; - export const useBeszelFilteredSystems = ( results: { integrationId: string; systems: BeszelSystemRow[] }[], statusFilter: string, diff --git a/packages/widgets/src/beszel/_shared/stats-view.tsx b/packages/widgets/src/beszel/_shared/stats-view.tsx new file mode 100644 index 000000000..48706d4ad --- /dev/null +++ b/packages/widgets/src/beszel/_shared/stats-view.tsx @@ -0,0 +1,339 @@ +"use client"; + +import { useMemo } from "react"; +import { Button, Center, Group, SimpleGrid, Skeleton, Stack, Text } from "@mantine/core"; +import { IconPlugConnectedX, IconServerOff } from "@tabler/icons-react"; + +import { clientApi } from "@homarr/api/client"; +import type { BeszelContainerStatsRecord, BeszelSystemStatsRecord } from "@homarr/integrations/types"; +import { useScopedI18n } from "@homarr/translation/client"; + +import { containerColors } from "./colors"; +import type { BeszelTimePeriod } from "./chart"; +import { + BeszelChartPanel, + CPU_Y_AXIS_DOMAIN, + useContainerNames, + useDiskChartData, + useDockerChartData, + useSystemChartData, +} from "./chart"; +import { chartAxisFormatters, formatByteRate, formatGB, formatPercent, formatStorageBytes } from "./format"; +import { makeTooltipProps } from "./tooltip"; +import { useLiveStats } from "./use-live-stats"; + +const CHART_HEIGHT = 180; +const MB = 1024 * 1024; +const tooltipPercent = makeTooltipProps(formatPercent); +const tooltipGB = makeTooltipProps(formatGB, true); +const tooltipRate = makeTooltipProps(formatByteRate, true); +const tooltipPercentTotal = makeTooltipProps(formatPercent, true); +const tooltipBytesTotal = makeTooltipProps(formatStorageBytes, true); + +const ChartSkeleton = () => ( + + + + + + + +); + +export interface BeszelStatsVisibility { + cpu: boolean; + memory: boolean; + disk: boolean; + diskIO: boolean; + network: boolean; + dockerCpu: boolean; + dockerMemory: boolean; + dockerNetwork: boolean; +} + +interface BeszelStatsViewProps { + integrationIds: string[]; + systemId: string; + timePeriod: BeszelTimePeriod; + visibility: BeszelStatsVisibility; + columns: 1 | 2; + onSwitchToHistorical?: () => void; +} + +const whenVisible = (visible: boolean, data: T | undefined) => (visible ? data : undefined); + +export function BeszelStatsView({ + integrationIds, + systemId, + timePeriod, + visibility, + columns, + onSwitchToHistorical, +}: BeszelStatsViewProps) { + const t = useScopedI18n("widget.beszelSystemStats"); + const showDocker = visibility.dockerCpu || visibility.dockerMemory || visibility.dockerNetwork; + const isLive = timePeriod === "1m"; + + const { data: historicalData, error: historicalError } = clientApi.widget.beszel.getSystemStats.useQuery( + { integrationIds, systemId, timePeriod, includeDocker: showDocker }, + { refetchInterval: isLive ? false : 5_000, enabled: !isLive && systemId !== "" }, + ); + const { data: liveData, error: liveError } = useLiveStats(integrationIds, systemId, isLive && systemId !== ""); + + const activeStats: + | { systemStats: BeszelSystemStatsRecord[]; containerStats: BeszelContainerStatsRecord[]; error?: string } + | null + | undefined = isLive ? liveData : historicalData; + + const mappers = useMemo( + () => ({ + cpu: (s: { cpu: number }) => ({ [t("chart.cpu.series")]: s.cpu }), + memory: (s: { mu: number; mb: number }) => ({ + [t("chart.memory.series")]: s.mu, + [t("chart.memory.cache")]: s.mb ?? 0, + }), + diskIO: (s: { dr?: number; dw?: number }) => ({ + [t("chart.diskIO.read")]: (s.dr ?? 0) * MB, + [t("chart.diskIO.write")]: (s.dw ?? 0) * MB, + }), + network: (s: { ns?: number; nr?: number; b?: [number, number] }) => ({ + [t("chart.network.sent")]: s.b?.[0] ?? s.ns ?? 0, + [t("chart.network.recv")]: s.b?.[1] ?? s.nr ?? 0, + }), + }), + [t], + ); + + const series = useMemo( + () => ({ + cpu: [{ name: t("chart.cpu.series"), color: "teal.6" }], + memory: [ + { name: t("chart.memory.series"), color: "teal.5" }, + { name: t("chart.memory.cache"), color: "teal.8" }, + ], + diskIO: [ + { name: t("chart.diskIO.write"), color: "orange.6" }, + { name: t("chart.diskIO.read"), color: "blue.6" }, + ], + network: [ + { name: t("chart.network.sent"), color: "blue.6" }, + { name: t("chart.network.recv"), color: "teal.6" }, + ], + }), + [t], + ); + + const systemStats = activeStats?.systemStats; + const containerStats = activeStats?.containerStats; + const cpuData = useSystemChartData(whenVisible(visibility.cpu, systemStats), mappers.cpu, timePeriod); + const memoryData = useSystemChartData(whenVisible(visibility.memory, systemStats), mappers.memory, timePeriod); + const diskIOData = useSystemChartData(whenVisible(visibility.diskIO, systemStats), mappers.diskIO, timePeriod); + const networkData = useSystemChartData(whenVisible(visibility.network, systemStats), mappers.network, timePeriod); + + const efsPaths = useMemo(() => { + const paths = new Set(); + for (const record of systemStats ?? []) { + for (const path of Object.keys(record.stats.efs ?? {})) paths.add(path); + } + return [...paths]; + }, [systemStats]); + const rootSeriesName = t("chart.disk.series"); + const diskData = useDiskChartData(whenVisible(visibility.disk, systemStats), efsPaths, rootSeriesName, timePeriod); + const diskSeries = useMemo( + () => [ + { name: rootSeriesName, color: "grape.6" }, + ...efsPaths.map((path, index) => ({ + name: path, + color: containerColors[(index + 1) % containerColors.length] as string, + })), + ], + [efsPaths, rootSeriesName], + ); + + const containerNames = useContainerNames(whenVisible(showDocker, containerStats)); + const containerSeries = useMemo( + () => + containerNames.map((name, index) => ({ name, color: containerColors[index % containerColors.length] as string })), + [containerNames], + ); + const dockerCpuData = useDockerChartData( + whenVisible(visibility.dockerCpu, containerStats), + containerNames, + "cpu", + timePeriod, + ); + const dockerMemoryData = useDockerChartData( + whenVisible(visibility.dockerMemory, containerStats), + containerNames, + "memory", + timePeriod, + ); + const dockerNetworkData = useDockerChartData( + whenVisible(visibility.dockerNetwork, containerStats), + containerNames, + "network", + timePeriod, + ); + + if (historicalError) throw historicalError; + + if (isLive && liveError) { + return ( +
+ + + + {t("error.liveConnectionFailed")} + + {onSwitchToHistorical && ( + + )} + +
+ ); + } + + if (!activeStats) { + const visibleChartCount = Object.values(visibility).filter(Boolean).length; + return ( + + {Array.from({ length: visibleChartCount }, (_, index) => ( + + ))} + + ); + } + + if ("error" in activeStats && activeStats.error) { + return ( +
+ + + + {activeStats.error} + + +
+ ); + } + + return ( + + {visibility.cpu && cpuData.length > 0 && ( + + )} + {visibility.memory && memoryData.length > 0 && ( + + )} + {visibility.disk && diskData.length > 0 && ( + + )} + {visibility.diskIO && diskIOData.length > 0 && ( + + )} + {visibility.network && networkData.length > 0 && ( + + )} + {showDocker && containerSeries.length > 0 && ( + <> + {visibility.dockerCpu && dockerCpuData.length > 0 && ( + + )} + {visibility.dockerMemory && dockerMemoryData.length > 0 && ( + + )} + {visibility.dockerNetwork && dockerNetworkData.length > 0 && ( + + )} + + )} + + ); +} diff --git a/packages/widgets/src/beszel/_shared/system-stats-modal.tsx b/packages/widgets/src/beszel/_shared/system-stats-modal.tsx index 52781c5af..fda3146d2 100644 --- a/packages/widgets/src/beszel/_shared/system-stats-modal.tsx +++ b/packages/widgets/src/beszel/_shared/system-stats-modal.tsx @@ -1,25 +1,15 @@ "use client"; -import { useMemo, useState } from "react"; -import { SegmentedControl, SimpleGrid, Stack } from "@mantine/core"; +import { useState } from "react"; +import { SegmentedControl, Stack } from "@mantine/core"; -import { clientApi } from "@homarr/api/client"; import { createModal } from "@homarr/modals"; -import { useScopedI18n } from "@homarr/translation/client"; import type { BeszelTimePeriod } from "./chart"; -import { BeszelChartPanel, CPU_Y_AXIS_DOMAIN, useSystemChartData } from "./chart"; -import { chartAxisFormatters, formatByteRate, formatGB, formatPercent } from "./format"; -import { makeTooltipProps } from "./tooltip"; - -const tooltipPercent = makeTooltipProps(formatPercent); -const tooltipGB = makeTooltipProps(formatGB, true); -const tooltipRate = makeTooltipProps(formatByteRate, true); - -const CHART_HEIGHT = 180; -const MB = 1024 * 1024; +import { BeszelStatsView } from "./stats-view"; const timePeriodOptions: { value: BeszelTimePeriod; label: string }[] = [ + { value: "1m", label: "Live" }, { value: "1h", label: "1H" }, { value: "12h", label: "12H" }, { value: "24h", label: "24H" }, @@ -27,144 +17,41 @@ const timePeriodOptions: { value: BeszelTimePeriod; label: string }[] = [ { value: "30d", label: "30D" }, ]; +const allCharts = { + cpu: true, + memory: true, + disk: true, + diskIO: true, + network: true, + dockerCpu: true, + dockerMemory: true, + dockerNetwork: true, +} as const; + interface BeszelSystemStatsModalProps { integrationId: string; systemId: string; } export const BeszelSystemStatsModal = createModal(({ innerProps }) => { - const t = useScopedI18n("widget.beszelSystemStats"); const [timePeriod, setTimePeriod] = useState("1h"); - const { data } = clientApi.widget.beszel.getSystemStats.useQuery( - { - integrationIds: [innerProps.integrationId], - systemId: innerProps.systemId, - timePeriod, - includeDocker: false, - }, - { staleTime: 10_000, gcTime: 48 * 60 * 60 * 1000, refetchInterval: 10_000 }, - ); - - const mappers = useMemo( - () => ({ - cpu: (s: { cpu: number }) => ({ [t("chart.cpu.series")]: s.cpu }), - memory: (s: { mu: number; mb: number }) => ({ - [t("chart.memory.series")]: s.mu, - [t("chart.memory.cache")]: s.mb ?? 0, - }), - disk: (s: { du: number }) => ({ [t("chart.disk.series")]: s.du }), - diskIO: (s: { dr?: number; dw?: number }) => ({ - [t("chart.diskIO.read")]: (s.dr ?? 0) * MB, - [t("chart.diskIO.write")]: (s.dw ?? 0) * MB, - }), - network: (s: { ns: number; nr: number; b?: [number, number] }) => ({ - [t("chart.network.sent")]: s.b?.[0] ?? s.ns, - [t("chart.network.recv")]: s.b?.[1] ?? s.nr, - }), - }), - [t], - ); - - const series = useMemo( - () => ({ - cpu: [{ name: t("chart.cpu.series"), color: "teal.6" }], - memory: [ - { name: t("chart.memory.series"), color: "teal.5" }, - { name: t("chart.memory.cache"), color: "teal.8" }, - ], - disk: [{ name: t("chart.disk.series"), color: "grape.6" }], - diskIO: [ - { name: t("chart.diskIO.write"), color: "orange.6" }, - { name: t("chart.diskIO.read"), color: "blue.6" }, - ], - network: [ - { name: t("chart.network.sent"), color: "blue.6" }, - { name: t("chart.network.recv"), color: "teal.6" }, - ], - }), - [t], - ); - - const systemStats = data?.systemStats; - const cpuData = useSystemChartData(systemStats, mappers.cpu, timePeriod); - const memoryData = useSystemChartData(systemStats, mappers.memory, timePeriod); - const diskData = useSystemChartData(systemStats, mappers.disk, timePeriod); - const diskIOData = useSystemChartData(systemStats, mappers.diskIO, timePeriod); - const networkData = useSystemChartData(systemStats, mappers.network, timePeriod); - return ( setTimePeriod(v as BeszelTimePeriod)} + onChange={(value) => setTimePeriod(value as BeszelTimePeriod)} data={timePeriodOptions} /> - - {cpuData.length > 0 && ( - - )} - {memoryData.length > 0 && ( - - )} - {diskData.length > 0 && ( - - )} - {diskIOData.length > 0 && ( - - )} - {networkData.length > 0 && ( - - )} - + setTimePeriod("1h")} + /> ); }).withOptions({ diff --git a/packages/widgets/src/beszel/_shared/use-live-stats.ts b/packages/widgets/src/beszel/_shared/use-live-stats.ts index 440800d0d..f2ab5df34 100644 --- a/packages/widgets/src/beszel/_shared/use-live-stats.ts +++ b/packages/widgets/src/beszel/_shared/use-live-stats.ts @@ -1,92 +1,65 @@ "use client"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { skipToken } from "@tanstack/react-query"; import { clientApi } from "@homarr/api/client"; import type { BeszelContainerStatsRecord, BeszelSystemStatsRecord } from "@homarr/integrations/types"; -const MAX_BUFFER = 30; -const FLUSH_INTERVAL_MS = 3000; - -interface LiveStatsData { - systemStats: BeszelSystemStatsRecord[]; - containerStats: BeszelContainerStatsRecord[]; -} - -function appendToBuffer( - prev: LiveStatsData | null, - incoming: { stats: BeszelSystemStatsRecord; containerStats: BeszelContainerStatsRecord | null }, -): LiveStatsData { - const systemStats = [...(prev?.systemStats ?? []), incoming.stats].slice(-MAX_BUFFER); - const prevContainer = prev?.containerStats ?? []; - if (!incoming.containerStats) { - return { systemStats, containerStats: prevContainer }; - } - return { systemStats, containerStats: [...prevContainer, incoming.containerStats].slice(-MAX_BUFFER) }; -} +// Beszel emits roughly one record per second. Keep a sliding one-minute window +// without synthesizing points for missed updates. +const MAX_BUFFER = 60; export const useLiveStats = (integrationIds: string[], systemId: string, enabled: boolean) => { - const [data, setData] = useState(null); + const [systemStats, setSystemStats] = useState([]); + const [containerStats, setContainerStats] = useState([]); const [error, setError] = useState(null); - const bufferRef = useRef(null); - const dirtyRef = useRef(false); - const generationRef = useRef(0); - const integrationKey = [...integrationIds].sort().join(","); - - useEffect(() => { - generationRef.current += 1; - setData(null); - setError(null); - bufferRef.current = null; - dirtyRef.current = false; - }, [systemId, enabled, integrationKey]); - - const onData = useCallback( - (incoming: { stats: BeszelSystemStatsRecord; containerStats: BeszelContainerStatsRecord | null }) => { - const gen = generationRef.current; - queueMicrotask(() => { - if (generationRef.current !== gen) return; - setError(null); - const isFirst = !bufferRef.current; - bufferRef.current = appendToBuffer(bufferRef.current, incoming); - dirtyRef.current = true; - if (isFirst) { - setData(bufferRef.current); - } - }); - }, - [], - ); - - const onError = useCallback((err: unknown) => { - if (err instanceof Error) { - setError(err); - return; - } - setError(new Error(String(err))); + // Append to buffer, trimming to MAX_BUFFER + const appendSystemStats = useCallback((record: BeszelSystemStatsRecord) => { + setSystemStats((prev) => { + const next = [...prev, record]; + return next.length > MAX_BUFFER ? next.slice(-MAX_BUFFER) : next; + }); }, []); - const subscriptionInput = useMemo(() => ({ integrationIds, systemId }), [integrationKey, systemId]); - const subscriptionEnabled = enabled && systemId !== ""; + const appendContainerStats = useCallback((record: BeszelContainerStatsRecord) => { + setContainerStats((prev) => { + const next = [...prev, record]; + return next.length > MAX_BUFFER ? next.slice(-MAX_BUFFER) : next; + }); + }, []); + clientApi.widget.beszel.subscribeSystemStats.useSubscription( + enabled && systemId !== "" ? { integrationIds, systemId } : skipToken, + { + onData(event) { + setError(null); + if (event.type === "system_stats") { + appendSystemStats(event.record); + } else { + appendContainerStats(event.record); + } + }, + onError(err) { + setError(err instanceof Error ? err : new Error(String(err))); + }, + }, + ); + + // Reset buffers when system changes or integration changes + const prevKeyRef = useRef(""); + const currentKey = `${integrationIds.join(",")}:${systemId}:${enabled}`; useEffect(() => { - if (!subscriptionEnabled) return; - const gen = generationRef.current; - const id = setInterval(() => { - if (generationRef.current !== gen) return; - if (!dirtyRef.current || !bufferRef.current) return; - dirtyRef.current = false; - setData(bufferRef.current); - }, FLUSH_INTERVAL_MS); - return () => clearInterval(id); - }, [subscriptionEnabled, integrationKey, systemId]); + if (prevKeyRef.current !== currentKey) { + prevKeyRef.current = currentKey; + setSystemStats([]); + setContainerStats([]); + setError(null); + } + }, [currentKey]); - clientApi.widget.beszel.subscribeSystemStats.useSubscription(subscriptionInput, { - enabled: subscriptionEnabled, - onData, - onError, - }); + const data = systemStats.length > 0 || containerStats.length > 0 ? { systemStats, containerStats } : null; return { data, error }; }; diff --git a/packages/widgets/src/calendar/calender-day.tsx b/packages/widgets/src/calendar/calender-day.tsx index 790cc0a82..70f2f26a8 100644 --- a/packages/widgets/src/calendar/calender-day.tsx +++ b/packages/widgets/src/calendar/calender-day.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useCallback, useState } from "react"; import { Box, Container, Flex, Popover, Text, useMantineTheme } from "@mantine/core"; import { useElementSize } from "@mantine/hooks"; @@ -13,10 +13,12 @@ interface CalendarDayProps { disabled: boolean; rootWidth: number; rootHeight: number; + onOpen: (close: () => void) => void; } -export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth }: CalendarDayProps) => { +export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth, onOpen }: CalendarDayProps) => { const [opened, setOpened] = useState(false); + const [pinned, setPinned] = useState(false); const { primaryColor } = useMantineTheme(); const { ref, height } = useElementSize(); const board = useRequiredBoard(); @@ -29,6 +31,19 @@ export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth }: C const isTooSmallForIndicators = height < 30; + const handleMouseEnter = useCallback(() => { + if (disabled) return; + onOpen(() => { + setOpened(false); + setPinned(false); + }); + setOpened(true); + }, [disabled, onOpen]); + + const handleMouseLeave = useCallback(() => { + if (!pinned) setOpened(false); + }, [pinned]); + return ( { + setOpened(value); + if (!value) setPinned(false); + }} opened={opened} disabled={disabled} > @@ -59,10 +77,10 @@ export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth }: C cursor: disabled ? "default" : "pointer", }} onClick={() => { - if (disabled) return; - - setOpened((prev) => !prev); + if (!disabled) setPinned((prev) => !prev); }} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} > {date.getDate()} @@ -71,7 +89,15 @@ export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth }: C {/* Popover has some offset on the left side, padding is removed because of scrollarea paddings */} - + setOpened(true)} + onMouseLeave={handleMouseLeave} + > diff --git a/packages/widgets/src/calendar/component.tsx b/packages/widgets/src/calendar/component.tsx index ac3d8b37b..b00cd1af0 100644 --- a/packages/widgets/src/calendar/component.tsx +++ b/packages/widgets/src/calendar/component.tsx @@ -1,6 +1,6 @@ "use client"; -import { useMemo, useState } from "react"; +import { useCallback, useMemo, useRef, useState } from "react"; import { useParams } from "next/navigation"; import { useMantineTheme } from "@mantine/core"; import { Calendar } from "@mantine/dates"; @@ -39,27 +39,7 @@ const FetchCalendar = ({ month, setMonth, isEditMode, integrationIds, options }: releaseType: options.releaseType, showUnmonitored: options.showUnmonitored, }; - const { data } = clientApi.widget.calendar.findAllEvents.useQuery(input, { - staleTime: 60 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }); - - const utils = clientApi.useUtils(); - clientApi.widget.calendar.subscribeToEvents.useSubscription(input, { - onData(data) { - utils.widget.calendar.findAllEvents.setData(input, (old) => { - return old?.map((item) => { - if (item.integration.id !== data.integration.id) return item; - return { - ...item, - events: data.events, - }; - }); - }); - }, - }); + const { data } = clientApi.widget.calendar.findAllEvents.useQuery(input); const events = useMemo(() => data?.flatMap((item) => item.events) ?? [], [data]); @@ -85,6 +65,11 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar const isSmall = width < 256; const normalizedEvents = useMemo(() => splitEvents(events), [events]); + const activeCloseRef = useRef<(() => void) | null>(null); + const onDayOpen = useCallback((close: () => void) => { + activeCloseRef.current?.(); + activeCloseRef.current = close; + }, []); return ( ); }} diff --git a/packages/widgets/src/coolify/component.tsx b/packages/widgets/src/coolify/component.tsx index 91b37ecd4..46940f56e 100644 --- a/packages/widgets/src/coolify/component.tsx +++ b/packages/widgets/src/coolify/component.tsx @@ -31,27 +31,7 @@ interface CoolifyContentProps { } function CoolifyContent({ integrationIds, options, width }: CoolifyContentProps) { - const { data: instancesData = [] } = clientApi.widget.coolify.getInstancesInfo.useQuery( - { integrationIds }, - { staleTime: 30 * 1000 }, - ); - - const utils = clientApi.useUtils(); - clientApi.widget.coolify.subscribeInstancesInfo.useSubscription( - { integrationIds }, - { - onData(newData) { - utils.widget.coolify.getInstancesInfo.setData({ integrationIds }, (prevData) => { - if (!prevData) return prevData; - return prevData.map((instance) => - instance.integrationId === newData.integrationId - ? { ...instance, instanceInfo: newData.instanceInfo, updatedAt: newData.timestamp } - : instance, - ); - }); - }, - }, - ); + const { data: instancesData = [] } = clientApi.widget.coolify.getInstancesInfo.useQuery({ integrationIds }); const isTiny = width < 256; const [firstInstance] = instancesData; diff --git a/packages/widgets/src/definition.ts b/packages/widgets/src/definition.ts index 4c65d231e..aaf60269f 100644 --- a/packages/widgets/src/definition.ts +++ b/packages/widgets/src/definition.ts @@ -1,5 +1,6 @@ +import type React from "react"; import type { LoaderComponent } from "next/dynamic"; -import type { QueryClient } from "@tanstack/react-query"; +import type { QueryClient, QueryKey } from "@tanstack/react-query"; import type { DefaultErrorData } from "@trpc/server/unstable-core-do-not-import"; import type { IntegrationKind, WidgetKind } from "@homarr/definitions"; @@ -9,7 +10,35 @@ import type { stringOrTranslation } from "@homarr/translation"; import type { TablerIcon } from "@homarr/ui"; import type { WidgetImports } from "."; -import type { inferOptionsFromCreator, WidgetOptionsRecord } from "./options"; +import type { inferOptionsFromCreator, inferOptionsFromDefinition, WidgetOptionsRecord } from "./options"; + +export interface WidgetContextMenuAction { + key: string; + label: stringOrTranslation; + icon?: TablerIcon; + onClick: () => void; + hidden?: boolean; + disabled?: boolean; + color?: string; +} + +export interface WidgetContextMenuContext { + isEditMode: boolean; + boardId: string | undefined; + itemId: string | undefined; +} + +export interface WidgetContextActionProps< + TKind extends WidgetKind, + TOptions extends WidgetOptionsRecord = WidgetOptionsRecord, +> { + kind: TKind; + options: inferOptionsFromDefinition; + setOptions: (partial: Partial>) => void; + integrationIds: string[]; + context: WidgetContextMenuContext; + widgetStateRef: React.MutableRefObject | null>; +} const createWithDynamicImport = (kind: TKind, definition: TDefinition) => @@ -40,6 +69,7 @@ export const createWidgetDefinition = >; + contextActions?: (props: Omit, "kind">) => WidgetContextMenuAction[]; } export interface WidgetProps { @@ -70,6 +101,7 @@ export type WidgetComponentProps = WidgetProps setOptions: ({ newOptions }: { newOptions: Partial>> }) => void; width: number; height: number; + widgetStateRef?: React.MutableRefObject | null>; }; export type WidgetOptionsRecordOf = WidgetImports[TKind]["definition"]["createOptions"]; diff --git a/packages/widgets/src/dns-hole/controls/component.tsx b/packages/widgets/src/dns-hole/controls/component.tsx index afe397cfd..f46c3d746 100644 --- a/packages/widgets/src/dns-hole/controls/component.tsx +++ b/packages/widgets/src/dns-hole/controls/component.tsx @@ -51,99 +51,16 @@ export default function DnsHoleControlsWidget({ .map(({ id }) => id) .filter((id) => integrationIds.includes(id)); - const { data: summaries = [] } = clientApi.widget.dnsHole.summary.useQuery( - { - integrationIds, - }, - { - staleTime: 5 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); + const { data: summaries = [] } = clientApi.widget.dnsHole.summary.useQuery({ + integrationIds, + }); const utils = clientApi.useUtils(); - // Subscribe to summary updates - clientApi.widget.dnsHole.subscribeToSummary.useSubscription( - { - integrationIds, - }, - { - onData: (data) => { - utils.widget.dnsHole.summary.setData( - { - integrationIds, - }, - (prevData) => { - if (!prevData) return undefined; - const newData = prevData.map((summary) => - summary.integration.id === data.integration.id - ? { - integration: { - ...summary.integration, - updatedAt: new Date(), - }, - summary: data.summary, - } - : summary, - ); - - return newData; - }, - ); - }, - }, - ); - - // Mutations for dnsHole state, set to undefined on click, and change again on settle const { mutate: enableDns } = clientApi.widget.dnsHole.enable.useMutation({ - onSettled: (_, error, { integrationId }) => { - utils.widget.dnsHole.summary.setData( - { - integrationIds, - }, - (prevData) => { - if (!prevData) return []; - - return prevData.map((item) => - item.integration.id === integrationId - ? { - ...item, - summary: { - ...item.summary, - status: error ? "disabled" : "enabled", - }, - } - : item, - ); - }, - ); - }, + onSettled: () => void utils.widget.dnsHole.summary.invalidate(), }); const { mutate: disableDns } = clientApi.widget.dnsHole.disable.useMutation({ - onSettled: (_, error, { integrationId }) => { - utils.widget.dnsHole.summary.setData( - { - integrationIds, - }, - (prevData) => { - if (!prevData) return []; - - return prevData.map((item) => - item.integration.id === integrationId - ? { - ...item, - summary: { - ...item.summary, - status: error ? "enabled" : "disabled", - }, - } - : item, - ); - }, - ); - }, + onSettled: () => void utils.widget.dnsHole.summary.invalidate(), }); const toggleDns = (integrationId: string) => { const integrationStatus = summaries.find(({ integration }) => integration.id === integrationId); diff --git a/packages/widgets/src/dns-hole/summary/component.tsx b/packages/widgets/src/dns-hole/summary/component.tsx index 4a4c2879c..4bcc82568 100644 --- a/packages/widgets/src/dns-hole/summary/component.tsx +++ b/packages/widgets/src/dns-hole/summary/component.tsx @@ -20,46 +20,12 @@ import type { widgetKind } from "."; import type { WidgetComponentProps, WidgetProps } from "../../definition"; export default function DnsHoleSummaryWidget({ options, integrationIds }: WidgetComponentProps) { - const { data: summaries = [] } = clientApi.widget.dnsHole.summary.useQuery( - { - integrationIds, - }, - { - staleTime: 5 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); - const utils = clientApi.useUtils(); + const { data: summaries = [] } = clientApi.widget.dnsHole.summary.useQuery({ + integrationIds, + }); const t = useI18n(); - clientApi.widget.dnsHole.subscribeToSummary.useSubscription( - { - integrationIds, - }, - { - onData: (data) => { - utils.widget.dnsHole.summary.setData( - { - integrationIds, - }, - (prevData) => { - if (!prevData) { - return undefined; - } - - const newData = prevData.map((item) => - item.integration.id === data.integration.id ? { ...item, summary: data.summary } : item, - ); - return newData; - }, - ); - }, - }, - ); - const data = useMemo(() => summaries.flatMap(({ summary }) => summary), [summaries]); return ( diff --git a/packages/widgets/src/docker/component.tsx b/packages/widgets/src/docker/component.tsx index d6c1d004d..42db221a2 100644 --- a/packages/widgets/src/docker/component.tsx +++ b/packages/widgets/src/docker/component.tsx @@ -1,10 +1,10 @@ "use client"; import { useMemo } from "react"; -import { ActionIcon, Avatar, Badge, Group, Stack, Text, Tooltip } from "@mantine/core"; +import { ActionIcon, Avatar, Badge, Center, Group, Stack, Text, Tooltip } from "@mantine/core"; import type { IconProps } from "@tabler/icons-react"; import { IconBrandDocker, IconPlayerPlay, IconPlayerStop, IconRotateClockwise } from "@tabler/icons-react"; -import type { MRT_ColumnDef } from "mantine-react-table"; +import type { MRT_ColumnDef, MRT_VisibilityState } from "mantine-react-table"; import { MantineReactTable } from "mantine-react-table"; import type { RouterOutputs } from "@homarr/api"; @@ -184,20 +184,13 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo const t = useScopedI18n("docker"); const isTiny = width <= 256; - const utils = clientApi.useUtils(); const { data } = clientApi.docker.getContainers.useQuery(undefined, { - staleTime: 20 * 1000, + refetchInterval: 30_000, }); const containers = data?.containers ?? []; const timestamp = useMemo(() => data?.timestamp ?? new Date(), [data?.timestamp]); const relativeTime = useTimeAgo(timestamp); - clientApi.docker.subscribeContainers.useSubscription(undefined, { - onData(data) { - utils.docker.getContainers.setData(undefined, { containers: data, timestamp: new Date() }); - }, - }); - const totalContainers = containers.length; const totals = useMemo(() => { @@ -213,6 +206,15 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo const columns = useMemo(() => createColumns(t), [t]); + const columnVisibility: MRT_VisibilityState = { + name: options.columns.includes("name"), + state: options.columns.includes("state"), + host: options.columns.includes("host"), + cpuUsage: options.columns.includes("cpuUsage"), + memoryUsage: options.columns.includes("memoryUsage"), + actions: options.columns.includes("actions"), + }; + const table = useTranslatedMantineReactTable({ columns, data: containers, @@ -233,6 +235,9 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo sorting: [{ id: options.defaultSort, desc: options.descendingDefaultSort }], density: "xs", }, + state: { + columnVisibility, + }, mantinePaperProps: { flex: 1, withBorder: false, @@ -260,6 +265,13 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo }, }); + if (options.columns.length === 0) + return ( +
+ {t("error.noColumns")} +
+ ); + return ( diff --git a/packages/widgets/src/docker/index.ts b/packages/widgets/src/docker/index.ts index 1a0964af3..76cdb97f3 100644 --- a/packages/widgets/src/docker/index.ts +++ b/packages/widgets/src/docker/index.ts @@ -12,10 +12,29 @@ const columnsList = [ "memoryUsage", ] as const satisfies (keyof RouterOutputs["docker"]["getContainers"]["containers"][number])[]; +const allColumnsList = ["name", "state", "host", "cpuUsage", "memoryUsage", "actions"] as const; + +const columnTranslationKeyMap = { + name: "docker.field.name.label", + state: "docker.field.state.label", + host: "docker.field.host.label", + cpuUsage: "docker.field.stats.cpu.label", + memoryUsage: "docker.field.stats.memory.label", + actions: "docker.action.title", +} as const satisfies Record<(typeof allColumnsList)[number], string>; + export const { definition, componentLoader } = createWidgetDefinition("dockerContainers", { icon: IconBrandDocker, createOptions() { return optionsBuilder.from((factory) => ({ + columns: factory.multiSelect({ + defaultValue: [...allColumnsList], + options: allColumnsList.map((value) => ({ + value, + label: (t) => t(columnTranslationKeyMap[value]), + })), + searchable: true, + }), enableRowSorting: factory.switch({ defaultValue: false, }), diff --git a/packages/widgets/src/downloads/component.tsx b/packages/widgets/src/downloads/component.tsx index c74723d86..0edfb9ba5 100644 --- a/packages/widgets/src/downloads/component.tsx +++ b/packages/widgets/src/downloads/component.tsx @@ -88,19 +88,10 @@ export default function DownloadClientsWidget({ integrationIds.includes(id) ? [id] : [], ); - const { data: currentItems = [] } = clientApi.widget.downloads.getJobsAndStatuses.useQuery( - { - integrationIds, - limitPerIntegration: options.limitPerIntegration, - }, - { - staleTime: 5 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); - const utils = clientApi.useUtils(); + const { data: currentItems = [] } = clientApi.widget.downloads.getJobsAndStatuses.useQuery({ + integrationIds, + limitPerIntegration: options.limitPerIntegration, + }); const t = useScopedI18n("widget.downloads"); const tCommon = useScopedI18n("common"); @@ -121,40 +112,13 @@ export default function DownloadClientsWidget({ return statuses; }, [currentItems]); - //Get API mutation functions - const { mutate: mutateResumeItem } = clientApi.widget.downloads.resumeItem.useMutation(); - const { mutate: mutatePauseItem } = clientApi.widget.downloads.pauseItem.useMutation(); - const { mutate: mutateDeleteItem } = clientApi.widget.downloads.deleteItem.useMutation(); + const utils = clientApi.useUtils(); + const invalidateDownloads = { onSettled: () => void utils.widget.downloads.getJobsAndStatuses.invalidate() }; + const { mutate: mutateResumeItem } = clientApi.widget.downloads.resumeItem.useMutation(invalidateDownloads); + const { mutate: mutatePauseItem } = clientApi.widget.downloads.pauseItem.useMutation(invalidateDownloads); + const { mutate: mutateDeleteItem } = clientApi.widget.downloads.deleteItem.useMutation(invalidateDownloads); - //Subscribe to dynamic data changes - clientApi.widget.downloads.subscribeToJobsAndStatuses.useSubscription( - { - integrationIds, - limitPerIntegration: options.limitPerIntegration, - }, - { - onData: (data) => { - utils.widget.downloads.getJobsAndStatuses.setData( - { integrationIds, limitPerIntegration: options.limitPerIntegration }, - (prevData) => { - return prevData?.map((item) => { - if (item.integration.id !== data.integration.id) return item; - - return { - data: data.data, - integration: { - ...data.integration, - updatedAt: new Date(), - }, - }; - }); - }, - ); - }, - }, - ); - - //Flatten Data array for which each element has it's integration, data (base + calculated) and actions. Memoized on data subscription + //Flatten Data array for which each element has its integration, data (base + calculated) and actions const data = useMemo( () => currentItems @@ -785,8 +749,10 @@ const ClientsControl = ({ clients, filters, setFilters, availableStatuses }: Cli "/s", ); - const { mutate: mutateResumeQueue } = clientApi.widget.downloads.resume.useMutation(); - const { mutate: mutatePauseQueue } = clientApi.widget.downloads.pause.useMutation(); + const utils = clientApi.useUtils(); + const invalidateDownloads = { onSettled: () => void utils.widget.downloads.getJobsAndStatuses.invalidate() }; + const { mutate: mutateResumeQueue } = clientApi.widget.downloads.resume.useMutation(invalidateDownloads); + const { mutate: mutatePauseQueue } = clientApi.widget.downloads.pause.useMutation(invalidateDownloads); const [opened, { open, close }] = useDisclosure(false); const t = useScopedI18n("widget.downloads"); return ( diff --git a/packages/widgets/src/filter-storage-volumes.spec.ts b/packages/widgets/src/filter-storage-volumes.spec.ts new file mode 100644 index 000000000..51d84169a --- /dev/null +++ b/packages/widgets/src/filter-storage-volumes.spec.ts @@ -0,0 +1,146 @@ +import { describe, expect, test } from "vitest"; + +import { + filterStorageVolumes, + normalizeStorageDeviceName, + toScopedStorageVolumeValue, +} from "./filter-storage-volumes"; +import { matchFileSystemAndSmart } from "./health-monitoring/system-health"; + +describe("normalizeStorageDeviceName", () => { + test("strips partition suffixes from block device paths", () => { + expect(normalizeStorageDeviceName("/dev/sda1")).toBe("/dev/sda"); + expect(normalizeStorageDeviceName("/dev/sda")).toBe("/dev/sda"); + }); + + test("preserves Synology-style volume names", () => { + expect(normalizeStorageDeviceName("volume_1")).toBe("volume_1"); + expect(normalizeStorageDeviceName("volume_2")).toBe("volume_2"); + }); + + test("strips NVMe partition suffixes without collapsing the namespace id", () => { + expect(normalizeStorageDeviceName("/dev/nvme0n1")).toBe("/dev/nvme0n1"); + expect(normalizeStorageDeviceName("/dev/nvme0n1p2")).toBe("/dev/nvme0n1"); + expect(normalizeStorageDeviceName("/dev/nvme0n2p1")).toBe("/dev/nvme0n2"); + }); + + test("does not collapse unrelated md devices", () => { + expect(normalizeStorageDeviceName("/dev/md0")).toBe("/dev/md0"); + expect(normalizeStorageDeviceName("/dev/md1")).toBe("/dev/md1"); + }); +}); + +describe("toScopedStorageVolumeValue", () => { + test("scopes unscoped volume names to the query integration", () => { + expect(toScopedStorageVolumeValue("nas-a", "volume_1")).toBe("nas-a:volume_1"); + }); + + test("re-scopes already-prefixed values using the query integration", () => { + expect(toScopedStorageVolumeValue("nas-b", "nas-a:volume_1")).toBe("nas-b:volume_1"); + }); +}); + +describe("filterStorageVolumes", () => { + const integrationId = "nas-1"; + + test("matches SMART entries using normalized partition identifiers", () => { + const entries = [ + { deviceName: "/dev/sda1", used: "1", available: "2", percentage: 50 }, + { deviceName: "/dev/sdb1", used: "1", available: "2", percentage: 50 }, + ]; + const smartEntries = [ + { deviceName: "/dev/sda", temperature: 35, overallStatus: "GOOD" }, + { deviceName: "/dev/sdb", temperature: 40, overallStatus: "GOOD" }, + ]; + + const visibleVolumes = [`${integrationId}:/dev/sda1`]; + + expect(filterStorageVolumes(entries, visibleVolumes, integrationId)).toHaveLength(1); + expect(filterStorageVolumes(smartEntries, visibleVolumes, integrationId)).toHaveLength(1); + }); + + test("keeps exact Synology volume matches", () => { + const entries = [{ deviceName: "volume_1" }, { deviceName: "volume_2" }]; + const visibleVolumes = [`${integrationId}:volume_1`]; + + expect(filterStorageVolumes(entries, visibleVolumes, integrationId)).toEqual([{ deviceName: "volume_1" }]); + }); + + test("does not apply another integration's scoped selection", () => { + const entries = [{ deviceName: "volume_1" }, { deviceName: "volume_2" }]; + const visibleVolumes = ["nas-b:volume_1"]; + + expect(filterStorageVolumes(entries, visibleVolumes, integrationId)).toEqual([]); + }); + + test("does not treat different md devices as the same volume", () => { + const entries = [{ deviceName: "/dev/md0" }, { deviceName: "/dev/md1" }]; + const visibleVolumes = [`${integrationId}:/dev/md0`]; + + expect(filterStorageVolumes(entries, visibleVolumes, integrationId)).toEqual([{ deviceName: "/dev/md0" }]); + }); +}); + +describe("matchFileSystemAndSmart", () => { + test("joins partitioned filesystem entries with base SMART device names", () => { + const result = matchFileSystemAndSmart( + [{ deviceName: "/dev/sda1", used: "100", available: "900", percentage: 10 }], + [{ deviceName: "/dev/sda", temperature: 42, overallStatus: "GOOD" }], + ); + + expect(result).toEqual([ + { + deviceName: "/dev/sda", + used: "100", + available: "900", + percentage: 10, + temperature: 42, + overallStatus: "GOOD", + }, + ]); + }); + + test("joins Synology volume names without stripping volume suffixes", () => { + const result = matchFileSystemAndSmart( + [{ deviceName: "volume_1", used: "100", available: "900", percentage: 10 }], + [{ deviceName: "volume_1", temperature: 39, overallStatus: "normal" }], + ); + + expect(result[0]).toMatchObject({ + deviceName: "volume_1", + temperature: 39, + overallStatus: "normal", + }); + }); + + test("joins NVMe partitions with their base device SMART data", () => { + const result = matchFileSystemAndSmart( + [{ deviceName: "/dev/nvme0n1p2", used: "100", available: "900", percentage: 10 }], + [{ deviceName: "/dev/nvme0n1", temperature: 41, overallStatus: "GOOD" }], + ); + + expect(result).toEqual([ + { + deviceName: "/dev/nvme0n1", + used: "100", + available: "900", + percentage: 10, + temperature: 41, + overallStatus: "GOOD", + }, + ]); + }); + + test("does not join SMART data from a different md device", () => { + const result = matchFileSystemAndSmart( + [{ deviceName: "/dev/md0", used: "100", available: "900", percentage: 10 }], + [{ deviceName: "/dev/md1", temperature: 41, overallStatus: "GOOD" }], + ); + + expect(result[0]).toMatchObject({ + deviceName: "/dev/md0", + temperature: null, + overallStatus: "", + }); + }); +}); diff --git a/packages/widgets/src/filter-storage-volumes.ts b/packages/widgets/src/filter-storage-volumes.ts new file mode 100644 index 000000000..5e88584be --- /dev/null +++ b/packages/widgets/src/filter-storage-volumes.ts @@ -0,0 +1,67 @@ +interface StorageVolumeEntry { + deviceName: string; +} + +const partitionSuffixPatterns: ReadonlyArray<{ pattern: RegExp; baseGroupIndex: number }> = [ + // SCSI/SATA/VirtIO: /dev/sda1 -> /dev/sda + { pattern: /^(\/dev\/(?:sd|vd|hd|xvd)[a-z]+)[0-9]+$/, baseGroupIndex: 1 }, + // NVMe: /dev/nvme0n1p2 -> /dev/nvme0n1 + { pattern: /^(\/dev\/nvme[0-9]+n[0-9]+)p[0-9]+$/, baseGroupIndex: 1 }, + // eMMC: /dev/mmcblk0p1 -> /dev/mmcblk0 + { pattern: /^(\/dev\/mmcblk[0-9]+)p[0-9]+$/, baseGroupIndex: 1 }, +]; + +export const normalizeStorageDeviceName = (deviceName: string): string => { + for (const { pattern, baseGroupIndex } of partitionSuffixPatterns) { + const match = deviceName.match(pattern); + if (match) { + return match[baseGroupIndex] ?? deviceName; + } + } + + return deviceName; +}; + +export const toScopedStorageVolumeValue = (integrationId: string, value: string): string => { + const separatorIndex = value.indexOf(":"); + const volumeName = separatorIndex === -1 ? value : value.slice(separatorIndex + 1); + return `${integrationId}:${volumeName}`; +}; + +const storageDeviceNamesMatch = (leftDeviceName: string, rightDeviceName: string): boolean => { + return ( + leftDeviceName === rightDeviceName || + normalizeStorageDeviceName(leftDeviceName) === normalizeStorageDeviceName(rightDeviceName) + ); +}; + +const matchesVisibleStorageVolume = ( + visibleVolume: string, + integrationId: string, + deviceName: string, +): boolean => { + const separatorIndex = visibleVolume.indexOf(":"); + if (separatorIndex === -1) { + return storageDeviceNamesMatch(visibleVolume, deviceName); + } + + const visibleIntegrationId = visibleVolume.slice(0, separatorIndex); + const visibleVolumeName = visibleVolume.slice(separatorIndex + 1); + return visibleIntegrationId === integrationId && storageDeviceNamesMatch(visibleVolumeName, deviceName); +}; + +export const filterStorageVolumes = ( + entries: TEntry[], + visibleStorageVolumes: string[] | undefined, + integrationId: string, +): TEntry[] => { + if (!visibleStorageVolumes || visibleStorageVolumes.length === 0) { + return entries; + } + + return entries.filter((entry) => + visibleStorageVolumes.some((visibleVolume) => + matchesVisibleStorageVolume(visibleVolume, integrationId, entry.deviceName), + ), + ); +}; diff --git a/packages/widgets/src/firewall/component.tsx b/packages/widgets/src/firewall/component.tsx index 7318cd3d3..523eadd8d 100644 --- a/packages/widgets/src/firewall/component.tsx +++ b/packages/widgets/src/firewall/component.tsx @@ -29,10 +29,16 @@ export default function FirewallWidget({ integrationIds, width, itemId }: Widget } }, []); - const firewallsCpuData = useUpdatingCpuStatus(integrationIds); - const firewallsMemoryData = useUpdatingMemoryStatus(integrationIds); - const firewallsVersionData = useUpdatingVersionStatus(integrationIds); - const firewallsInterfacesData = useUpdatingInterfacesStatus(integrationIds); + const { data: firewallsCpuData = [] } = clientApi.widget.firewall.getFirewallCpuStatus.useQuery({ integrationIds }); + const { data: firewallsMemoryData = [] } = clientApi.widget.firewall.getFirewallMemoryStatus.useQuery({ + integrationIds, + }); + const { data: firewallsVersionData = [] } = clientApi.widget.firewall.getFirewallVersionStatus.useQuery({ + integrationIds, + }); + const { data: firewallsInterfacesData = [] } = clientApi.widget.firewall.getFirewallInterfacesStatus.useQuery({ + integrationIds, + }); const initialSelectedFirewall = firewallsVersionData[0] ? firewallsVersionData[0].integration.id : "undefined"; const isTiny = width < 256; @@ -182,168 +188,6 @@ export default function FirewallWidget({ integrationIds, width, itemId }: Widget ); } -export const useUpdatingCpuStatus = (integrationIds: string[]) => { - const utils = clientApi.useUtils(); - const { data: firewallsCpuData = [] } = clientApi.widget.firewall.getFirewallCpuStatus.useQuery( - { - integrationIds, - }, - { - staleTime: 5 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); - - clientApi.widget.firewall.subscribeFirewallCpuStatus.useSubscription( - { - integrationIds, - }, - { - onData: (data) => { - utils.widget.firewall.getFirewallCpuStatus.setData( - { - integrationIds, - }, - (prevData) => { - if (!prevData) { - return undefined; - } - - return prevData.map((item) => - item.integration.id === data.integration.id ? { ...item, summary: data.summary } : item, - ); - }, - ); - }, - }, - ); - - return firewallsCpuData; -}; - -export const useUpdatingMemoryStatus = (integrationIds: string[]) => { - const utils = clientApi.useUtils(); - const { data: firewallsMemoryData = [] } = clientApi.widget.firewall.getFirewallMemoryStatus.useQuery( - { - integrationIds, - }, - { - staleTime: 15 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); - - clientApi.widget.firewall.subscribeFirewallMemoryStatus.useSubscription( - { - integrationIds, - }, - { - onData: (data) => { - utils.widget.firewall.getFirewallMemoryStatus.setData( - { - integrationIds, - }, - (prevData) => { - if (!prevData) { - return undefined; - } - - return prevData.map((item) => - item.integration.id === data.integration.id ? { ...item, summary: data.summary } : item, - ); - }, - ); - }, - }, - ); - - return firewallsMemoryData; -}; - -export const useUpdatingVersionStatus = (integrationIds: string[]) => { - const utils = clientApi.useUtils(); - const { data: firewallsVersionData = [] } = clientApi.widget.firewall.getFirewallVersionStatus.useQuery( - { - integrationIds, - }, - { - staleTime: 60 * 60 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); - - clientApi.widget.firewall.subscribeFirewallVersionStatus.useSubscription( - { - integrationIds, - }, - { - onData: (data) => { - utils.widget.firewall.getFirewallVersionStatus.setData( - { - integrationIds, - }, - (prevData) => { - if (!prevData) { - return undefined; - } - - return prevData.map((item) => - item.integration.id === data.integration.id ? { ...item, summary: data.summary } : item, - ); - }, - ); - }, - }, - ); - return firewallsVersionData; -}; - -export const useUpdatingInterfacesStatus = (integrationIds: string[]) => { - const utils = clientApi.useUtils(); - const { data: firewallsInterfacesData = [] } = clientApi.widget.firewall.getFirewallInterfacesStatus.useQuery( - { - integrationIds, - }, - { - staleTime: 30 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); - - clientApi.widget.firewall.subscribeFirewallInterfacesStatus.useSubscription( - { - integrationIds, - }, - { - onData: (data) => { - utils.widget.firewall.getFirewallInterfacesStatus.setData( - { - integrationIds, - }, - (prevData) => { - if (!prevData) { - return undefined; - } - return prevData.map((item) => - item.integration.id === data.integration.id ? { ...item, summary: data.summary } : item, - ); - }, - ); - }, - }, - ); - - return firewallsInterfacesData; -}; - export function formatBitsPerSec(bytes: number, decimals: number): string { if (bytes === 0) return "0 b/s"; diff --git a/packages/widgets/src/health-monitoring/cluster/cluster-health.tsx b/packages/widgets/src/health-monitoring/cluster/cluster-health.tsx index daa0b40ec..1ac7922d5 100644 --- a/packages/widgets/src/health-monitoring/cluster/cluster-health.tsx +++ b/packages/widgets/src/health-monitoring/cluster/cluster-health.tsx @@ -35,27 +35,9 @@ export const ClusterHealthMonitoring = ({ width, }: WidgetComponentProps<"healthMonitoring"> & { integrationId: string }) => { const t = useI18n(); - const { data: healthData } = clientApi.widget.healthMonitoring.getClusterHealthStatus.useQuery( - { - integrationId, - }, - { - staleTime: 5 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); - - const utils = clientApi.useUtils(); - clientApi.widget.healthMonitoring.subscribeClusterHealthStatus.useSubscription( - { integrationId }, - { - onData(data) { - utils.widget.healthMonitoring.getClusterHealthStatus.setData({ integrationId }, data); - }, - }, - ); + const { data: healthData } = clientApi.widget.healthMonitoring.getClusterHealthStatus.useQuery({ + integrationId, + }); if (!healthData) return ; diff --git a/packages/widgets/src/health-monitoring/index.ts b/packages/widgets/src/health-monitoring/index.ts index dd176a189..fe31514de 100644 --- a/packages/widgets/src/health-monitoring/index.ts +++ b/packages/widgets/src/health-monitoring/index.ts @@ -4,6 +4,7 @@ import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { createWidgetDefinition } from "../definition"; import { optionsBuilder } from "../options"; +import { createStorageVolumeMultiSelectOptions } from "../storage-volume-options"; export const { definition, componentLoader } = createWidgetDefinition("healthMonitoring", { icon: IconHeartRateMonitor, @@ -28,6 +29,7 @@ export const { definition, componentLoader } = createWidgetDefinition("healthMon fileSystem: factory.switch({ defaultValue: true, }), + visibleStorageVolumes: factory.integrationMultiSelect(createStorageVolumeMultiSelectOptions()), visibleClusterSections: factory.multiSelect({ options: [ { @@ -77,6 +79,11 @@ export const { definition, componentLoader } = createWidgetDefinition("healthMon return integrationKinds.every((kind) => kind === "proxmox") || integrationKinds.length === 0; }, }, + visibleStorageVolumes: { + shouldHide(_, integrationKinds) { + return integrationKinds.length === 0 || !integrationKinds.every((kind) => kind === "synology"); + }, + }, showUptime: { shouldHide(_, integrationKinds) { // Uptime is only shown on cluster health tab @@ -104,7 +111,7 @@ export const { definition, componentLoader } = createWidgetDefinition("healthMon }, ); }, - supportedIntegrations: getIntegrationKindsByCategory("healthMonitoring"), + supportedIntegrations: getIntegrationKindsByCategory("healthMonitoring").filter((kind) => kind !== "patchmon"), errors: { INTERNAL_SERVER_ERROR: { icon: IconServerOff, diff --git a/packages/widgets/src/health-monitoring/system-health.tsx b/packages/widgets/src/health-monitoring/system-health.tsx index 405838cac..3119adead 100644 --- a/packages/widgets/src/health-monitoring/system-health.tsx +++ b/packages/widgets/src/health-monitoring/system-health.tsx @@ -36,8 +36,9 @@ import { humanFileSize } from "@homarr/common"; import type { TranslationFunction } from "@homarr/translation"; import { useI18n } from "@homarr/translation/client"; -import type { WidgetComponentProps } from "../definition"; +import { filterStorageVolumes, normalizeStorageDeviceName } from "../filter-storage-volumes"; import { WidgetEmptyState } from "../common/empty-state"; +import type { WidgetComponentProps } from "../definition"; import { CpuRing } from "./rings/cpu-ring"; import { CpuTempRing } from "./rings/cpu-temp-ring"; import { GpuRing } from "./rings/gpu-ring"; @@ -52,39 +53,12 @@ export const SystemHealthMonitoring = ({ width, }: WidgetComponentProps<"healthMonitoring">) => { const t = useI18n(); - const { data: healthData = [] } = clientApi.widget.healthMonitoring.getSystemHealthStatus.useQuery( - { - integrationIds, - }, - { - staleTime: 5 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); + const { data: healthData = [] } = clientApi.widget.healthMonitoring.getSystemHealthStatus.useQuery({ + integrationIds, + }); const [opened, { open, close }] = useDisclosure(false); - const utils = clientApi.useUtils(); const board = useRequiredBoard(); - clientApi.widget.healthMonitoring.subscribeSystemHealthStatus.useSubscription( - { integrationIds }, - { - onData(data) { - utils.widget.healthMonitoring.getSystemHealthStatus.setData({ integrationIds }, (prevData) => { - if (!prevData) { - return undefined; - } - return prevData.map((item) => - item.integrationId === data.integrationId - ? { ...item, healthInfo: data.healthInfo, updatedAt: data.timestamp } - : item, - ); - }); - }, - }, - ); - const isTiny = width < 256; if (healthData.length === 0) return ; @@ -92,7 +66,13 @@ export const SystemHealthMonitoring = ({ return ( {healthData.map(({ integrationId, integrationName, healthInfo }) => { - const disksData = matchFileSystemAndSmart(healthInfo.fileSystem, healthInfo.smart); + const filteredFileSystem = filterStorageVolumes( + healthInfo.fileSystem, + options.visibleStorageVolumes, + integrationId, + ); + const filteredSmart = filterStorageVolumes(healthInfo.smart, options.visibleStorageVolumes, integrationId); + const disksData = matchFileSystemAndSmart(filteredFileSystem, filteredSmart); const memoryUsage = formatMemoryUsage(healthInfo.memAvailableInBytes, healthInfo.memUsedInBytes); return ( { return fileSystems .map((fileSystem) => { - const baseDeviceName = fileSystem.deviceName.replace(/[0-9]+$/, ""); - const smartDisk = smartData.find((smart) => smart.deviceName === baseDeviceName); + const normalizedFileSystemName = normalizeStorageDeviceName(fileSystem.deviceName); + const smartDisk = smartData.find( + (smart) => + smart.deviceName === fileSystem.deviceName || + normalizeStorageDeviceName(smart.deviceName) === normalizedFileSystemName, + ); return { deviceName: smartDisk?.deviceName ?? fileSystem.deviceName, diff --git a/packages/widgets/src/immich/album-carousel/component.tsx b/packages/widgets/src/immich/album-carousel/component.tsx index 94795991c..ee7085757 100644 --- a/packages/widgets/src/immich/album-carousel/component.tsx +++ b/packages/widgets/src/immich/album-carousel/component.tsx @@ -1,7 +1,7 @@ "use client"; import type { Dispatch, SetStateAction } from "react"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Box, Center, Group, Image, Stack, Text } from "@mantine/core"; import { IconAlertCircle, IconCalendar } from "@tabler/icons-react"; @@ -23,9 +23,18 @@ export default function ImmichAlbumCarouselWidget({ integrationId: integrationIds[0] ?? "", albumId: options.albumId ?? "", }, - { enabled: Boolean(options.albumId), staleTime: 15 * 60 * 1000 }, + { enabled: Boolean(options.albumId) }, ); + const photoAssets = useMemo(() => { + const assets = album?.assets.filter((asset) => asset.type === "IMAGE") ?? []; + return options.randomizePhotos ? shuffle(assets) : assets; + }, [album?.assets, options.randomizePhotos]); + + useEffect(() => { + setCurrentPhotoIndex(0); + }, [photoAssets]); + if (!options.albumId) { return ; } @@ -36,8 +45,6 @@ export default function ImmichAlbumCarouselWidget({ return ; } - const photoAssets = album.assets.filter((asset) => asset.type === "IMAGE"); - if (photoAssets.length === 0) { return ; } @@ -53,10 +60,22 @@ export default function ImmichAlbumCarouselWidget({ ); } +function shuffle(items: T[]) { + const shuffled = [...items]; + for (let index = shuffled.length - 1; index > 0; index--) { + const randomIndex = Math.floor(Math.random() * (index + 1)); + const current = shuffled[index]; + const random = shuffled[randomIndex]; + if (current === undefined || random === undefined) continue; + shuffled[index] = random; + shuffled[randomIndex] = current; + } + return shuffled; +} + interface CarouselProps { assets: { id: string; - deviceAssetId: string; originalPath: string; fileModifiedAt: string; publicLink: string; diff --git a/packages/widgets/src/immich/album-carousel/index.ts b/packages/widgets/src/immich/album-carousel/index.ts index 6ad37c50a..9f8110ffc 100644 --- a/packages/widgets/src/immich/album-carousel/index.ts +++ b/packages/widgets/src/immich/album-carousel/index.ts @@ -1,6 +1,8 @@ import { IconPhoto } from "@tabler/icons-react"; import z from "zod"; +import { clientApi } from "@homarr/api/client"; + import { createWidgetDefinition } from "../../definition"; import { optionsBuilder } from "../../options"; @@ -10,9 +12,20 @@ export const { definition, componentLoader } = createWidgetDefinition("immich-al integrationsRequired: true, createOptions() { return optionsBuilder.from((factory) => ({ - albumId: factory.text({ - defaultValue: "", + albumId: factory.integrationSelect({ withDescription: true, + clearable: true, + useOptions: (integrationIds: string[]) => { + const { + data = [], + isPending, + isError, + } = clientApi.widget.immich.getAlbums.useQuery( + { integrationId: integrationIds[0] ?? "" }, + { enabled: integrationIds.length > 0, staleTime: 15 * 60 * 1000 }, + ); + return { data: data.map((album) => ({ value: album.id, label: album.albumName })), isPending, isError }; + }, }), rotationIntervalSeconds: factory.number({ defaultValue: 5, @@ -23,6 +36,10 @@ export const { definition, componentLoader } = createWidgetDefinition("immich-al defaultValue: false, withDescription: true, }), + randomizePhotos: factory.switch({ + defaultValue: false, + withDescription: true, + }), })); }, }).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/immich/server-stats/component.tsx b/packages/widgets/src/immich/server-stats/component.tsx index e3e1596ea..919b0caf0 100644 --- a/packages/widgets/src/immich/server-stats/component.tsx +++ b/packages/widgets/src/immich/server-stats/component.tsx @@ -17,12 +17,9 @@ export default function ImmichServerStatsWidget({ options, }: WidgetComponentProps<"immich-serverStats">) { const t = useI18n(); - const { data: stats } = clientApi.widget.immich.getServerStats.useQuery( - { - integrationId: integrationIds[0] ?? "", - }, - { staleTime: 15 * 60 * 1000 }, - ); + const { data: stats } = clientApi.widget.immich.getServerStats.useQuery({ + integrationId: integrationIds[0] ?? "", + }); if (!stats) return ; diff --git a/packages/widgets/src/index.tsx b/packages/widgets/src/index.tsx index 70089d48f..1e9f89277 100644 --- a/packages/widgets/src/index.tsx +++ b/packages/widgets/src/index.tsx @@ -12,6 +12,7 @@ import * as audioStats from "./audio-stats"; import * as app from "./app"; import * as archiveTeamWarrior from "./archive-team-warrior"; import * as bookmarks from "./bookmarks"; +import * as bazarr from "./bazarr"; import * as calendar from "./calendar"; import * as clock from "./clock"; import * as coolify from "./coolify"; @@ -32,11 +33,13 @@ import * as mediaRequestsList from "./media-requests/list"; import * as mediaRequestsStats from "./media-requests/stats"; import * as mediaServer from "./media-server"; import * as mediaTranscoding from "./media-transcoding"; +import * as mediaMissing from "./media-missing"; import * as minecraftServerStatus from "./minecraft/server-status"; import * as networkControllerStatus from "./network-controller/network-status"; import * as networkControllerSummary from "./network-controller/summary"; import * as notebook from "./notebook"; import * as paperlessNgx from "./paperless-ngx"; +import * as patchmon from "./patchmon"; import * as notifications from "./notifications"; import type { WidgetOptionDefinition } from "./options"; import * as releases from "./releases"; @@ -49,6 +52,7 @@ import * as stockPrice from "./stocks"; import * as systemDisks from "./system-disks"; import * as systemResources from "./system-resources"; import * as timetable from "./timetable"; +import * as traefik from "./traefik"; import * as tracearr from "./tracearr"; import * as umami from "./umami"; import * as ups from "./ups"; @@ -62,7 +66,12 @@ import * as weather from "./weather"; import * as customApi from "./custom-api"; import * as openWebUi from "./open-webui"; -export type { WidgetDefinition, WidgetOptionsSettings } from "./definition"; +export type { + WidgetDefinition, + WidgetContextMenuAction, + WidgetContextActionProps, + WidgetOptionsSettings, +} from "./definition"; export type { WidgetComponentProps }; export type { WidgetOptionDefinition, WidgetOptionType } from "./options"; @@ -85,10 +94,12 @@ export const widgetImports = { downloads, "mediaRequests-requestList": mediaRequestsList, "mediaRequests-requestStats": mediaRequestsStats, + mediaMissing, networkControllerSummary, networkControllerStatus, rssFeed, bookmarks, + bazarr, indexerManager, healthMonitoring, mediaTranscoding, @@ -105,6 +116,7 @@ export const widgetImports = { "immich-serverStats": immichServerStats, "immich-albumCarousel": immichAlbumCarousel, paperlessNgx, + patchmon, tracearr, speedtestTracker, uptimeKuma, @@ -116,6 +128,7 @@ export const widgetImports = { beszelSystemGrid, beszelAlerts, beszelSystemStats, + traefik, customApi, openWebUi, } satisfies WidgetImportRecord; diff --git a/packages/widgets/src/indexer-manager/component.tsx b/packages/widgets/src/indexer-manager/component.tsx index 51ddb1805..4e3fb7c7e 100644 --- a/packages/widgets/src/indexer-manager/component.tsx +++ b/packages/widgets/src/indexer-manager/component.tsx @@ -18,33 +18,13 @@ export default function IndexerManagerWidget({ height, }: WidgetComponentProps<"indexerManager">) { const t = useI18n(); - const { data: indexersData = [] } = clientApi.widget.indexerManager.getIndexersStatus.useQuery( - { integrationIds }, - { - staleTime: 5 * 60 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); + const { data: indexersData = [] } = clientApi.widget.indexerManager.getIndexersStatus.useQuery({ integrationIds }); + const utils = clientApi.useUtils(); - - const { mutate: testAll, isPending } = clientApi.widget.indexerManager.testAllIndexers.useMutation(); + const { mutate: testAll, isPending } = clientApi.widget.indexerManager.testAllIndexers.useMutation({ + onSettled: () => void utils.widget.indexerManager.getIndexersStatus.invalidate(), + }); const board = useRequiredBoard(); - - clientApi.widget.indexerManager.subscribeIndexersStatus.useSubscription( - { integrationIds }, - { - onData(newData) { - utils.widget.indexerManager.getIndexersStatus.setData({ integrationIds }, (previousData) => - previousData?.map((item) => - item.integrationId === newData.integrationId ? { ...item, indexers: newData.indexers } : item, - ), - ); - }, - }, - ); - const hasSmallWidth = width < 256; const hasSmallHeight = height < 256; diff --git a/packages/widgets/src/media-missing/component.module.css b/packages/widgets/src/media-missing/component.module.css new file mode 100644 index 000000000..5b1c9f568 --- /dev/null +++ b/packages/widgets/src/media-missing/component.module.css @@ -0,0 +1,49 @@ +.card { + position: relative; + overflow: hidden; + display: flex; + text-decoration: none; + color: inherit; + border: 1px solid var(--border-color); + transition: + border-color 120ms ease, + transform 120ms ease, + box-shadow 120ms ease; + + @mixin dark { + --border-color: var(--mantine-color-dark-4); + } + @mixin light { + --border-color: var(--mantine-color-gray-3); + } + + &:hover { + border-color: var(--mantine-primary-color-filled); + transform: translateY(-1px); + box-shadow: var(--mantine-shadow-sm); + } +} + +.backdrop { + position: absolute; + inset: 0; + background-size: cover; + background-position: center; + filter: blur(20px) saturate(1.2); + transform: scale(1.3); + opacity: 0.16; + pointer-events: none; +} + +.content { + position: relative; + z-index: 1; + flex: 1; + min-width: 0; +} + +.poster { + flex-shrink: 0; + object-fit: cover; + box-shadow: var(--mantine-shadow-sm); +} diff --git a/packages/widgets/src/media-missing/component.tsx b/packages/widgets/src/media-missing/component.tsx new file mode 100644 index 000000000..69c1edf21 --- /dev/null +++ b/packages/widgets/src/media-missing/component.tsx @@ -0,0 +1,217 @@ +"use client"; + +import type { ReactNode } from "react"; +import { Badge, Box, Center, Group, Image, Paper, RingProgress, ScrollArea, SimpleGrid, Stack, Tabs, Text, ThemeIcon } from "@mantine/core"; +import { IconDownload, IconMovie, IconQuestionMark, IconVideo } from "@tabler/icons-react"; + +import { clientApi } from "@homarr/api/client"; +import type { MissingMediaItem, QueuedMediaItem } from "@homarr/integrations/types"; +import { useScopedI18n } from "@homarr/translation/client"; + +import { WidgetEmptyState } from "../common/empty-state"; +import type { WidgetComponentProps } from "../definition"; +import { NoIntegrationDataError } from "../errors/no-data-integration"; +import classes from "./component.module.css"; + +export default function MediaMissingWidget({ integrationIds, options, width, height }: WidgetComponentProps<"mediaMissing">) { + const t = useScopedI18n("widget.mediaMissing"); + const pageSize = Number(options.pageSize); + const { data } = clientApi.widget.mediaOrganizer.getData.useQuery( + { integrationIds, pageSize }, + { staleTime: 60 * 1000, refetchOnWindowFocus: false, refetchOnReconnect: false }, + ); + + if (!data) return ; + if (data.length === 0) throw new NoIntegrationDataError(); + if (!options.showMissing && !options.showQueued) + return ( +
+ + {t("empty.noTabsEnabled")} + +
+ ); + + const missing = data.flatMap((entry) => + entry.missing.map((item) => ({ item, integrationId: entry.integrationId })), + ); + const queued = data.flatMap((entry) => + entry.queued.map((item) => ({ item, integrationId: entry.integrationId })), + ); + const missingCount = data.reduce((sum, entry) => sum + entry.missingCount, 0); + const queuedCount = data.reduce((sum, entry) => sum + entry.queuedCount, 0); + + const isThin = width > 0 && width < 160; + const isShort = height > 0 && height < 180; + const targetCardWidth = isShort ? 130 : 200; + const columns = width > 0 ? Math.max(1, Math.min(Math.floor(width / targetCardWidth), 4)) : 1; + const density: Density = isThin ? "thin" : width > 0 && width / columns < 180 ? "compact" : "comfortable"; + + const tabLabel = (label: string, shown: number, total: number) => + isThin ? total : `${label} (${shown}/${total})`; + + const renderPanel = (entries: { item: MissingMediaItem | QueuedMediaItem; integrationId: string }[], emptyLabel: string) => ( + + + {entries.length === 0 ? ( + + {emptyLabel} + + ) : ( + + {entries.map(({ item, integrationId }) => ( + + ))} + + )} + + + ); + + return ( + + + {options.showMissing && ( + }> + {tabLabel(t("tab.missing"), missing.length, missingCount)} + + )} + {options.showQueued && ( + }> + {tabLabel(t("tab.queued"), queued.length, queuedCount)} + + )} + + + {options.showMissing && ( + + {renderPanel(missing, t("empty.missing"))} + + )} + {options.showQueued && ( + + {renderPanel(queued, t("empty.queued"))} + + )} + + ); +} + +type Density = "thin" | "compact" | "comfortable"; + +const CARD_HEIGHT: Record = { thin: 52, compact: 56, comfortable: 68 }; + +const posterSizes: Record = { thin: 34, compact: 40, comfortable: 52 }; + +const Poster = ({ src, type, density }: { src?: string | null; type: "movie" | "episode"; density: Density }) => { + const size = posterSizes[density]; + const w = Math.round(size * 0.68); + + if (src) { + return ; + } + + return ( + + {type === "movie" ? : } + + ); +}; + +const episodeCode = (item: MissingMediaItem | QueuedMediaItem) => + item.type === "episode" && item.seasonNumber !== undefined && item.episodeNumber !== undefined + ? `S${String(item.seasonNumber).padStart(2, "0")}E${String(item.episodeNumber).padStart(2, "0")}` + : null; + +const TypeBadge = ({ item, density }: { item: MissingMediaItem | QueuedMediaItem; density: Density }) => { + const t = useScopedI18n("widget.mediaMissing"); + const color = item.type === "movie" ? "yellow" : "blue"; + const code = episodeCode(item); + + if (density !== "comfortable") { + return ( + + {code ?? t(`type.${item.type}`)} + + ); + } + + return ( + + + {t(`type.${item.type}`)} + + {code && ( + + {code} + + )} + + ); +}; + +const primaryTitle = (item: MissingMediaItem | QueuedMediaItem) => + item.type === "episode" ? (item.seriesTitle ?? item.title) : item.title; + +const progressColor = (percent: number) => (percent >= 90 ? "green" : percent >= 40 ? "cyan" : "orange"); + +const ringSizes: Record = { + thin: { size: 30, thickness: 3, fz: "8px" }, + compact: { size: 34, thickness: 3, fz: "9px" }, + comfortable: { size: 40, thickness: 4, fz: "10px" }, +}; + +const ProgressRing = ({ percent, density }: { percent: number; density: Density }) => { + const color = progressColor(percent); + const ring = ringSizes[density]; + return ( + + {percent} + + } + /> + ); +}; + +const CardShell = ({ item, density, children }: { item: MissingMediaItem | QueuedMediaItem; density: Density; children: ReactNode }) => ( + + {item.imageUrl && ( + + )} +
{children}
+
+); + +const MediaCard = ({ item, density }: { item: MissingMediaItem | QueuedMediaItem; density: Density }) => { + const isQueued = "percentComplete" in item; + + return ( + + + + + + + {primaryTitle(item)} + + {density === "comfortable" && ( + + {item.type === "episode" ? item.title : item.year} + + )} + + {isQueued && } + + + ); +}; diff --git a/packages/widgets/src/media-missing/index.ts b/packages/widgets/src/media-missing/index.ts new file mode 100644 index 000000000..eaa44be6d --- /dev/null +++ b/packages/widgets/src/media-missing/index.ts @@ -0,0 +1,26 @@ +import { IconMovie } from "@tabler/icons-react"; + +import { getIntegrationKindsByCategory } from "@homarr/definitions"; + +import { createWidgetDefinition } from "../definition"; +import { optionsBuilder } from "../options"; + +const pageSizeOptions = ["10", "20", "30", "50"] as const; + +export const { componentLoader, definition } = createWidgetDefinition("mediaMissing", { + icon: IconMovie, + createOptions() { + return optionsBuilder.from((factory) => ({ + showMissing: factory.switch({ defaultValue: true }), + showQueued: factory.switch({ defaultValue: true }), + pageSize: factory.select({ + defaultValue: "10", + options: pageSizeOptions.map((value) => ({ + value, + label: value, + })), + }), + })); + }, + supportedIntegrations: getIntegrationKindsByCategory("mediaOrganizer"), +}).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/media-releases/component.tsx b/packages/widgets/src/media-releases/component.tsx index 61fc9216d..05ba18fe5 100644 --- a/packages/widgets/src/media-releases/component.tsx +++ b/packages/widgets/src/media-releases/component.tsx @@ -6,7 +6,7 @@ import { IconBook, IconCalendar, IconClock, IconStarFilled } from "@tabler/icons import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; -import { getMantineColor } from "@homarr/common"; +import { getMantineColor, toValidDate } from "@homarr/common"; import { getIconUrl } from "@homarr/definitions"; import type { MediaRelease } from "@homarr/integrations/types"; import { mediaTypeConfigurations } from "@homarr/integrations/types"; @@ -18,12 +18,9 @@ import { OverflowBadge } from "@homarr/ui"; import type { WidgetComponentProps } from "../definition"; export default function MediaReleasesWidget({ options, integrationIds }: WidgetComponentProps<"mediaReleases">) { - const { data: releases = [] } = clientApi.widget.mediaRelease.getMediaReleases.useQuery( - { - integrationIds, - }, - { staleTime: 5 * 60 * 1000 }, - ); + const { data: releases = [] } = clientApi.widget.mediaRelease.getMediaReleases.useQuery({ + integrationIds, + }); return ( @@ -42,6 +39,19 @@ interface ItemProps { options: WidgetComponentProps<"mediaReleases">["options"]; } +const formatReleaseDate = (value: unknown, locale: string) => { + const date = toValidDate(value); + if (!date) return "—"; + return Intl.DateTimeFormat(locale, { + month: "2-digit", + year: "numeric", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + hour12: false, + }).format(date); +}; + const Item = ({ item, options }: ItemProps) => { const locale = useCurrentLocale(); const t = useI18n(); @@ -94,17 +104,7 @@ const Item = ({ item, options }: ItemProps) => { )} - + {length !== undefined && ( <> diff --git a/packages/widgets/src/media-requests/list/component.tsx b/packages/widgets/src/media-requests/list/component.tsx index 3bc24cf70..bbbd0047e 100644 --- a/packages/widgets/src/media-requests/list/component.tsx +++ b/packages/widgets/src/media-requests/list/component.tsx @@ -19,6 +19,7 @@ import { IconSearch, IconThumbDown, IconThumbUp } from "@tabler/icons-react"; import type { RouterInputs, RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; import { useRequiredBoard } from "@homarr/boards/context"; +import { toValidDate } from "@homarr/common"; import type { MediaRequestStatus } from "@homarr/integrations/types"; import { mediaAvailabilityConfiguration, mediaRequestStatusConfiguration } from "@homarr/integrations/types"; import { openMediaRequestSearch } from "@homarr/spotlight"; @@ -35,44 +36,9 @@ export default function MediaServerWidget({ options, width, }: WidgetComponentProps<"mediaRequests-requestList">) { - const { data: mediaRequests } = clientApi.widget.mediaRequests.getLatestRequests.useQuery( - { - integrationIds, - }, - { - staleTime: 60 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - }, - ); - const utils = clientApi.useUtils(); - clientApi.widget.mediaRequests.subscribeToLatestRequests.useSubscription( - { - integrationIds, - }, - { - onData(data) { - utils.widget.mediaRequests.getLatestRequests.setData({ integrationIds }, (prevData) => { - if (!prevData) return []; - - const filteredData = prevData.filter(({ integrationId }) => integrationId !== data.integrationId); - const newData = filteredData.concat( - data.requests.map((request) => ({ ...request, integrationId: data.integrationId })), - ); - return newData.toSorted((dataA, dataB) => { - if (dataA.status === dataB.status) { - return dataB.createdAt.getTime() - dataA.createdAt.getTime(); - } - - return ( - mediaRequestStatusConfiguration[dataA.status].position - - mediaRequestStatusConfiguration[dataB.status].position - ); - }); - }); - }, - }, - ); + const { data: mediaRequests } = clientApi.widget.mediaRequests.getLatestRequests.useQuery({ + integrationIds, + }); if (!mediaRequests) return ; if (mediaRequests.length === 0) throw new NoIntegrationDataError(); @@ -169,7 +135,7 @@ const MediaRequestCard = ({ request, isTiny, options }: MediaRequestCardProps) = - {request.airDate?.getFullYear() ?? t("toBeDetermined")} + {toValidDate(request.airDate)?.getFullYear() ?? t("toBeDetermined")} {!isTiny && ( { - const { mutate: mutateRequestAnswer } = clientApi.widget.mediaRequests.answerRequest.useMutation(); + const utils = clientApi.useUtils(); + const { mutate: mutateRequestAnswer } = clientApi.widget.mediaRequests.answerRequest.useMutation({ + onSettled: () => void utils.widget.mediaRequests.invalidate(), + }); const t = useScopedI18n("widget.mediaRequests-requestList"); const handleDecision = (answer: RouterInputs["widget"]["mediaRequests"]["answerRequest"]["answer"]) => { mutateRequestAnswer({ diff --git a/packages/widgets/src/media-requests/list/index.ts b/packages/widgets/src/media-requests/list/index.ts index 8f1a88575..d4e0fcd54 100644 --- a/packages/widgets/src/media-requests/list/index.ts +++ b/packages/widgets/src/media-requests/list/index.ts @@ -1,6 +1,7 @@ -import { IconZoomQuestion } from "@tabler/icons-react"; +import { IconSearch, IconZoomQuestion } from "@tabler/icons-react"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; +import { openMediaRequestSearch } from "@homarr/spotlight"; import { createWidgetDefinition } from "../../definition"; import { optionsBuilder } from "../../options"; @@ -14,5 +15,15 @@ export const { componentLoader, definition } = createWidgetDefinition("mediaRequ }), })); }, + contextActions: ({ integrationIds }) => [ + { + key: "search", + label: (t) => t("search.mode.media.action.search.label"), + icon: IconSearch, + onClick: () => { + openMediaRequestSearch({ integrationIds }); + }, + }, + ], supportedIntegrations: getIntegrationKindsByCategory("mediaRequest"), }).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/media-requests/stats/component.tsx b/packages/widgets/src/media-requests/stats/component.tsx index 51a3e3d5e..5348b845a 100644 --- a/packages/widgets/src/media-requests/stats/component.tsx +++ b/packages/widgets/src/media-requests/stats/component.tsx @@ -36,16 +36,9 @@ export default function MediaServerWidget({ width, }: WidgetComponentProps<"mediaRequests-requestStats">) { const t = useScopedI18n("widget.mediaRequests-requestStats"); - const { data: requestStats } = clientApi.widget.mediaRequests.getStats.useQuery( - { - integrationIds, - }, - { - staleTime: 60 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - }, - ); + const { data: requestStats } = clientApi.widget.mediaRequests.getStats.useQuery({ + integrationIds, + }); const board = useRequiredBoard(); diff --git a/packages/widgets/src/media-requests/stats/index.ts b/packages/widgets/src/media-requests/stats/index.ts index 25911c894..d49cb8495 100644 --- a/packages/widgets/src/media-requests/stats/index.ts +++ b/packages/widgets/src/media-requests/stats/index.ts @@ -1,6 +1,7 @@ -import { IconChartBar } from "@tabler/icons-react"; +import { IconChartBar, IconSearch } from "@tabler/icons-react"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; +import { openMediaRequestSearch } from "@homarr/spotlight"; import { createWidgetDefinition } from "../../definition"; @@ -9,5 +10,15 @@ export const { componentLoader, definition } = createWidgetDefinition("mediaRequ createOptions() { return {}; }, + contextActions: ({ integrationIds }) => [ + { + key: "search", + label: (t) => t("search.mode.media.action.search.label"), + icon: IconSearch, + onClick: () => { + openMediaRequestSearch({ integrationIds }); + }, + }, + ], supportedIntegrations: getIntegrationKindsByCategory("mediaRequest"), }).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/media-server/component.tsx b/packages/widgets/src/media-server/component.tsx index efa2c2415..e63c12a90 100644 --- a/packages/widgets/src/media-server/component.tsx +++ b/packages/widgets/src/media-server/component.tsx @@ -18,23 +18,11 @@ import { useTranslatedMantineReactTable } from "@homarr/ui/hooks"; import type { WidgetComponentProps } from "../definition"; -export default function MediaServerWidget({ - options, - integrationIds, - isEditMode, -}: WidgetComponentProps<"mediaServer">) { - const { data: currentStreams = [] } = clientApi.widget.mediaServer.getCurrentStreams.useQuery( - { - integrationIds, - showOnlyPlaying: options.showOnlyPlaying, - }, - { - staleTime: 5 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - }, - ); - const utils = clientApi.useUtils(); +export default function MediaServerWidget({ options, integrationIds }: WidgetComponentProps<"mediaServer">) { + const { data: currentStreams = [] } = clientApi.widget.mediaServer.getCurrentStreams.useQuery({ + integrationIds, + showOnlyPlaying: options.showOnlyPlaying, + }); const t = useScopedI18n("widget.mediaServer"); const columns = useMemo[]>( @@ -83,32 +71,6 @@ export default function MediaServerWidget({ [t], ); - clientApi.widget.mediaServer.subscribeToCurrentStreams.useSubscription( - { - integrationIds, - showOnlyPlaying: options.showOnlyPlaying, - }, - { - enabled: !isEditMode, - onData(data) { - utils.widget.mediaServer.getCurrentStreams.setData( - { integrationIds, showOnlyPlaying: options.showOnlyPlaying }, - (previousData) => { - return previousData?.map((pair) => { - if (pair.integrationId === data.integrationId) { - return { - ...pair, - sessions: data.data, - }; - } - return pair; - }); - }, - ); - }, - }, - ); - // Only render the flat list of sessions when the currentStreams change // Otherwise it will always create a new array reference and cause the table to re-render const flatSessions = useMemo( diff --git a/packages/widgets/src/media-transcoding/component.tsx b/packages/widgets/src/media-transcoding/component.tsx index 8b0ba6591..fe4873e45 100644 --- a/packages/widgets/src/media-transcoding/component.tsx +++ b/packages/widgets/src/media-transcoding/component.tsx @@ -38,18 +38,7 @@ export default function MediaTranscodingWidget({ pageSize: queuePageSize, page: queuePage, }; - const { data: transcodingData } = clientApi.widget.mediaTranscoding.getDataAsync.useQuery(input, { - staleTime: 5 * 60 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - }); - - const utils = clientApi.useUtils(); - clientApi.widget.mediaTranscoding.subscribeData.useSubscription(input, { - onData(data) { - utils.widget.mediaTranscoding.getDataAsync.setData(input, data); - }, - }); + const { data: transcodingData } = clientApi.widget.mediaTranscoding.getDataAsync.useQuery(input); const [view, setView] = useState(options.defaultView); const t = useI18n("widget.mediaTranscoding"); diff --git a/packages/widgets/src/minecraft/server-status/component.tsx b/packages/widgets/src/minecraft/server-status/component.tsx index ddf4d6e4b..20214e3ac 100644 --- a/packages/widgets/src/minecraft/server-status/component.tsx +++ b/packages/widgets/src/minecraft/server-status/component.tsx @@ -11,18 +11,7 @@ import { WidgetEmptyState } from "../../common/empty-state"; import type { WidgetComponentProps } from "../../definition"; export default function MinecraftServerStatusWidget({ options }: WidgetComponentProps<"minecraftServerStatus">) { - const { data: result } = clientApi.widget.minecraft.getServerStatus.useQuery(options, { - staleTime: 5 * 60 * 1000, - }); - const utils = clientApi.useUtils(); - clientApi.widget.minecraft.subscribeServerStatus.useSubscription(options, { - onData(data) { - utils.widget.minecraft.getServerStatus.setData(options, { - data, - timestamp: new Date(), - }); - }, - }); + const { data: result } = clientApi.widget.minecraft.getServerStatus.useQuery(options); const tStatus = useScopedI18n("widget.minecraftServerStatus.status"); if (!result) return ; diff --git a/packages/widgets/src/modals/widget-edit-modal.tsx b/packages/widgets/src/modals/widget-edit-modal.tsx index be0d3a178..3edb6ffe5 100644 --- a/packages/widgets/src/modals/widget-edit-modal.tsx +++ b/packages/widgets/src/modals/widget-edit-modal.tsx @@ -19,6 +19,7 @@ import { getInputForType } from "../_inputs"; import { FormProvider, useForm } from "../_inputs/form"; import type { BoardItemAdvancedOptions } from "../../../validation/src/shared"; import type { OptionsBuilderResult } from "../options"; +import { OPTIONS_SUPER_REFINE } from "../options"; import type { IntegrationSelectOption } from "../widget-integration-select"; import { WidgetIntegrationSelect } from "../widget-integration-select"; import { WidgetAdvancedOptionsModal } from "./widget-advanced-options-modal"; @@ -38,6 +39,7 @@ interface ModalProps { integrationData: IntegrationSelectOption[]; integrationSupport: boolean; settings: SettingsContextProps; + itemId?: string; appId?: string; } @@ -53,24 +55,29 @@ export const WidgetEditModal = createModal>(({ actions, i }); const { definition } = widgetImports[innerProps.kind]; const options = definition.createOptions(innerProps.settings) as Record; + const optionsSuperRefine = (options as Record)[OPTIONS_SUPER_REFINE] as + | ((data: Record, ctx: z.RefinementCtx) => void) + | undefined; + const optionsFieldSchema = objectEntries(options).reduce( + (acc, [key, value]: [string, { type: string; validate?: z.ZodType }]) => { + if (value.validate) { + acc[key] = value.type === "multiText" ? z.array(value.validate).optional() : value.validate; + } + + return acc; + }, + {} as Record>, + ); + const optionsSchema = optionsSuperRefine + ? z.object(optionsFieldSchema).superRefine(optionsSuperRefine) + : z.object(optionsFieldSchema); const form = useForm({ mode: "controlled", initialValues: innerProps.value, validate: schemaResolver( z.object({ - options: z.object( - objectEntries(options).reduce( - (acc, [key, value]: [string, { type: string; validate?: z.ZodType }]) => { - if (value.validate) { - acc[key] = value.type === "multiText" ? z.array(value.validate).optional() : value.validate; - } - - return acc; - }, - {} as Record>, - ), - ), + options: optionsSchema, integrationIds: z.array(z.string()), advancedOptions: z.object({ customCssClasses: z.array(z.string()), @@ -146,6 +153,7 @@ export const WidgetEditModal = createModal>(({ actions, i property={key} options={value as never} initialOptions={innerProps.value.options} + itemId={innerProps.itemId} /> ); })} diff --git a/packages/widgets/src/network-controller/network-status/component.tsx b/packages/widgets/src/network-controller/network-status/component.tsx index 846d1128f..c4e892dba 100644 --- a/packages/widgets/src/network-controller/network-status/component.tsx +++ b/packages/widgets/src/network-controller/network-status/component.tsx @@ -21,48 +21,9 @@ export default function NetworkControllerNetworkStatusWidget({ options, integrationIds, }: WidgetComponentProps<"networkControllerStatus">) { - const { data: summaries = [] } = clientApi.widget.networkController.summary.useQuery( - { - integrationIds, - }, - { - staleTime: 5 * 60 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); - const utils = clientApi.useUtils(); - - clientApi.widget.networkController.subscribeToSummary.useSubscription( - { - integrationIds, - }, - { - onData: (data) => { - utils.widget.networkController.summary.setData( - { - integrationIds, - }, - (prevData) => { - if (!prevData) { - return undefined; - } - - return prevData.map((item) => - item.integration.id === data.integration.id - ? { - ...item, - summary: data.summary, - updatedAt: new Date(), - } - : item, - ); - }, - ); - }, - }, - ); + const { data: summaries = [] } = clientApi.widget.networkController.summary.useQuery({ + integrationIds, + }); const data = useMemo(() => summaries.flatMap(({ summary }) => summary), [summaries]); diff --git a/packages/widgets/src/network-controller/summary/component.tsx b/packages/widgets/src/network-controller/summary/component.tsx index 21a4b6a93..cb5dc7545 100644 --- a/packages/widgets/src/network-controller/summary/component.tsx +++ b/packages/widgets/src/network-controller/summary/component.tsx @@ -20,45 +20,12 @@ dayjs.extend(duration); export default function NetworkControllerSummaryWidget({ integrationIds, }: WidgetComponentProps<"networkControllerSummary">) { - const { data: summaries = [] } = clientApi.widget.networkController.summary.useQuery( - { - integrationIds, - }, - { - staleTime: 5 * 60 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); - const utils = clientApi.useUtils(); + const { data: summaries = [] } = clientApi.widget.networkController.summary.useQuery({ + integrationIds, + }); const t = useI18n(); - clientApi.widget.networkController.subscribeToSummary.useSubscription( - { - integrationIds, - }, - { - onData: (data) => { - utils.widget.networkController.summary.setData( - { - integrationIds, - }, - (prevData) => { - if (!prevData) { - return undefined; - } - - return prevData.map((item) => - item.integration.id === data.integration.id ? { ...item, summary: data.summary } : item, - ); - }, - ); - }, - }, - ); - const data = useMemo(() => summaries.flatMap(({ summary }) => summary), [summaries]); return ( diff --git a/packages/widgets/src/notebook/notebook.tsx b/packages/widgets/src/notebook/notebook.tsx index 97721daf7..229b3730a 100644 --- a/packages/widgets/src/notebook/notebook.tsx +++ b/packages/widgets/src/notebook/notebook.tsx @@ -1,5 +1,6 @@ "use client"; +import type { MouseEvent as ReactMouseEvent } from "react"; import { useCallback, useEffect, useRef, useState } from "react"; import { ActionIcon, @@ -263,8 +264,21 @@ export function Notebook({ options, setOptions, isEditMode, boardId, itemId }: W setIsEditing(handleEditToggleCallback); }, [setIsEditing, handleEditToggleCallback]); + const handleDoubleClick = useCallback( + (event: ReactMouseEvent) => { + if (!canChange || isEditing) return; + // Ignore double-clicks bubbling up from interactive controls (e.g. the + // edit/save ActionIcon), which would otherwise toggle edit mode twice. + if (event.target instanceof Element && event.target.closest("button, a")) { + return; + } + setIsEditing(handleEditToggleCallback); + }, + [canChange, isEditing, setIsEditing, handleEditToggleCallback], + ); + return ( - + ) { - const { data: notificationIntegrations = [] } = clientApi.widget.notifications.getNotifications.useQuery( - { - ...options, - integrationIds, - }, - { - staleTime: 5 * 60 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); - const utils = clientApi.useUtils(); - - clientApi.widget.notifications.subscribeNotifications.useSubscription( - { - ...options, - integrationIds, - }, - { - onData: (data) => { - utils.widget.notifications.getNotifications.setData({ ...options, integrationIds }, (prevData) => { - return prevData?.map((item) => { - if (item.integration.id !== data.integration.id) return item; - - return { - data: data.data, - integration: { - ...data.integration, - updatedAt: new Date(), - }, - }; - }); - }); - }, - }, - ); + const { data: notificationIntegrations = [] } = clientApi.widget.notifications.getNotifications.useQuery({ + ...options, + integrationIds, + }); const t = useScopedI18n("widget.notifications"); @@ -67,18 +34,39 @@ export default function NotificationsWidget({ options, integrationIds }: WidgetC {sortedNotifications.length > 0 ? ( sortedNotifications.map((notification) => ( - - - {notification.title && ( - - {notification.title} - + + + {!options.hideLogos && notification.source?.iconUrl && ( + )} - - {notification.body} - - + + {notification.title && ( + + {notification.title} + + )} + + {notification.body} + + + + )) diff --git a/packages/widgets/src/notifications/index.ts b/packages/widgets/src/notifications/index.ts index 577d698a0..553ef5d9f 100644 --- a/packages/widgets/src/notifications/index.ts +++ b/packages/widgets/src/notifications/index.ts @@ -8,7 +8,9 @@ import { optionsBuilder } from "../options"; export const { componentLoader, definition } = createWidgetDefinition("notifications", { icon: IconMessage, createOptions() { - return optionsBuilder.from(() => ({})); + return optionsBuilder.from((factory) => ({ + hideLogos: factory.switch({ defaultValue: false }), + })); }, supportedIntegrations: getIntegrationKindsByCategory("notifications"), }).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/options.ts b/packages/widgets/src/options.ts index 1d19f628a..c699e68da 100644 --- a/packages/widgets/src/options.ts +++ b/packages/widgets/src/options.ts @@ -2,7 +2,7 @@ import type React from "react"; import type { DraggableAttributes, UniqueIdentifier } from "@dnd-kit/core"; import type { ActionIconProps } from "@mantine/core"; import { z } from "zod/v4"; -import type { ZodType } from "zod/v4"; +import type { RefinementCtx, ZodType } from "zod/v4"; import type { IntegrationKind } from "@homarr/definitions"; @@ -13,6 +13,7 @@ import type { ReleasesRepository } from "./releases/releases-repository"; interface CommonInput { defaultValue?: TType; withDescription?: boolean; + skipContextMenu?: boolean; } interface TextInput extends CommonInput { @@ -54,6 +55,7 @@ interface DynamicSelectInput extends CommonInput { useOptions: ( query: string, integrationIds: string[], + options: Record, ) => { isPending: boolean; options: DynamicSelectOption[]; @@ -70,6 +72,15 @@ interface IntegrationSelectInput extends CommonInput { }; } +interface IntegrationMultiSelectInput extends CommonInput { + withDescription?: boolean; + useOptions: (integrationIds: string[]) => { + data: { value: string; label: string }[]; + isPending: boolean; + isError: boolean; + }; +} + interface NumberInput extends CommonInput { validate: z.ZodNumber; step?: number; @@ -91,6 +102,7 @@ const optionsFactory = { type: "switch" as const, defaultValue: input?.defaultValue ?? false, withDescription: input?.withDescription ?? false, + skipContextMenu: input?.skipContextMenu ?? false, }), text: (input?: TextInput) => ({ type: "text" as const, @@ -193,6 +205,12 @@ const optionsFactory = { searchable: input.searchable ?? true, useOptions: input.useOptions, }), + integrationMultiSelect: (input: IntegrationMultiSelectInput) => ({ + type: "integrationMultiSelect" as const, + defaultValue: input.defaultValue ?? [], + withDescription: input.withDescription ?? false, + useOptions: input.useOptions, + }), customWidgetSelect: (input?: CommonInput) => ({ type: "customWidgetSelect" as const, defaultValue: input?.defaultValue ?? "", @@ -239,9 +257,12 @@ type ConfigurationInput = Partial< Record> >; +export const OPTIONS_SUPER_REFINE = Symbol("optionsSuperRefine"); + const createOptions = ( optionsCallback: (factory: WidgetOptionFactory) => TOptions, configuration?: ConfigurationInput, + optionsSuperRefine?: (data: inferOptionsFromDefinition, ctx: RefinementCtx) => void, ) => { const obj = {} as Record; const options = optionsCallback(optionsFactory); @@ -253,9 +274,18 @@ const createOptions = ( }; } - return obj as { + const result = obj as { [key in keyof TOptions]: TOptions[key] & FieldConfiguration; }; + + if (optionsSuperRefine) { + Object.defineProperty(result, OPTIONS_SUPER_REFINE, { + value: optionsSuperRefine, + enumerable: false, + }); + } + + return result; }; type OptionsBuilder = typeof createOptions; diff --git a/packages/widgets/src/paperless-ngx/component.tsx b/packages/widgets/src/paperless-ngx/component.tsx index 3b476aa0d..08bcee3fb 100644 --- a/packages/widgets/src/paperless-ngx/component.tsx +++ b/packages/widgets/src/paperless-ngx/component.tsx @@ -72,10 +72,9 @@ const heroPartVisibility = { export default function PaperlessNgxWidget({ integrationIds, options, width }: WidgetComponentProps<"paperlessNgx">) { const t = useScopedI18n("widget.paperlessNgx"); - const { data: stats } = clientApi.widget.paperlessNgx.getStats.useQuery( - { integrationId: integrationIds[0] ?? "" }, - { staleTime: 5 * 60 * 1000 }, - ); + const { data: stats } = clientApi.widget.paperlessNgx.getStats.useQuery({ + integrationId: integrationIds[0] ?? "", + }); if (!stats) return ; diff --git a/packages/widgets/src/patchmon/component.module.css b/packages/widgets/src/patchmon/component.module.css new file mode 100644 index 000000000..bb7dc2784 --- /dev/null +++ b/packages/widgets/src/patchmon/component.module.css @@ -0,0 +1,371 @@ +.root { + container-type: size; + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + padding: clamp(6px, 3cqi, 14px); + gap: clamp(4px, 2cqi, 10px); + overflow: hidden; +} + +.compactRoot { + padding: clamp(4px, 3cqi, 8px); + gap: 2px; +} + +.content { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + gap: clamp(4px, 2cqi, 8px); +} + +.hero { + display: flex; + align-items: center; + justify-content: space-between; + gap: clamp(8px, 3cqi, 18px); + flex-shrink: 0; + padding-bottom: clamp(4px, 2cqi, 10px); + border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4)); +} + +.heroExpanded { + flex: 1; + border-bottom: none; + justify-content: center; + padding-bottom: 0; +} + +.heroTextOnly { + justify-content: center; +} + +.heroRingOnly { + justify-content: center; + padding-bottom: 0; + border-bottom: none; +} + +.heroWithText { + justify-content: center; + width: 100%; +} + +.heroWithText .heroText { + flex: 0 1 auto; +} + +.heroExpanded.heroRingOnly { + flex: 1; +} + +.heroTextOnly .heroText { + align-items: center; + text-align: center; +} + +.heroTextOnly .heroValue { + font-size: clamp(22px, 10cqi, 36px); +} + +.heroText { + display: flex; + flex-direction: column; + gap: clamp(2px, 1cqi, 4px); + min-width: 0; + flex: 1; +} + +.heroLabel { + font-size: clamp(9px, 2.8cqi, 13px); + color: var(--mantine-color-dimmed); + line-height: 1.2; +} + +.heroValue { + font-size: clamp(18px, 8cqi, 32px); + font-weight: 700; + line-height: 1.1; +} + +.ring { + flex-shrink: 0; +} + +.compactPrimary { + display: flex; + flex: 1; + align-items: center; + justify-content: center; + min-height: 0; + min-width: 0; +} + +.statsSection { + flex: 1; + min-height: 0; +} + +.statsSectionCompact { + flex: 0 1 auto; +} + +.grid { + display: grid; + height: 100%; + min-height: 0; + gap: clamp(4px, 2cqi, 10px); + grid-template-columns: repeat(var(--stat-cols, 2), minmax(0, 1fr)); + grid-auto-rows: 1fr; + align-content: stretch; +} + +.statTile { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 0; + min-width: 0; + gap: clamp(2px, 1.5cqmin, 5px); + padding: clamp(4px, 2.5cqi, 10px); + border-radius: var(--mantine-radius-md); + background-color: var(--stat-bg, light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6))); + overflow: hidden; +} + +.statIcon { + flex-shrink: 0; +} + +.statLabel { + font-size: clamp(8px, 2.5cqmin, 12px); + color: var(--mantine-color-dimmed); + line-height: 1.2; + text-align: center; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.statValue { + font-size: clamp(14px, 5.5cqmin, 24px); + font-weight: 700; + line-height: 1.1; + text-align: center; +} + +.compactStatGrid { + display: grid; + flex: 1; + grid-template-columns: repeat(var(--compact-stat-cols, 1), minmax(0, 1fr)); + grid-auto-rows: minmax(0, 1fr); + gap: clamp(3px, 2cqi, 6px); + min-width: 0; + min-height: 0; + width: 100%; +} + +.compactStatTile { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-width: 0; + min-height: 0; + padding: clamp(4px, 3cqi, 8px); + border-radius: var(--mantine-radius-md); + background-color: var(--stat-bg, light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6))); + overflow: hidden; +} + +.compactStatValue { + font-size: clamp(22px, 17cqmin, 42px); + font-weight: 700; + line-height: 1; + text-align: center; +} + +.compactStatLabel { + max-width: 100%; + overflow: hidden; + color: var(--mantine-color-dimmed); + font-size: clamp(8px, 2.4cqmin, 11px); + line-height: 1.1; + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; +} + +.emptyState { + display: flex; + flex: 1; + align-items: center; + justify-content: center; + min-height: 0; + color: var(--mantine-color-dimmed); + font-size: clamp(11px, 3cqi, 14px); + text-align: center; +} + +.osSectionWrapper { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; +} + +.osSectionWithStats { + padding-top: clamp(4px, 2cqi, 8px); + border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4)); +} + +.osSection { + min-height: 0; +} + +.osHeading { + flex-shrink: 0; +} + +.osScroll { + flex: 1; + min-height: 0; +} + +.osRow { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.osLabel { + font-size: clamp(9px, 2.5cqmin, 12px); + line-height: 1.2; +} + +.osBarGroup { + width: 100%; + min-width: 0; +} + +.osBar { + flex: 1; + min-width: 0; +} + +.osCount { + flex: 0 0 auto; + min-width: 1.5rem; + text-align: right; + font-size: clamp(9px, 2.5cqmin, 12px); +} + +.compactOsBars { + display: flex; + flex: 1; + flex-direction: column; + justify-content: center; + gap: clamp(3px, 2cqmin, 5px); + min-width: 0; + min-height: 0; + width: 100%; +} + +.compactOsBar { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.compactOsLabel { + max-width: 100%; + overflow: hidden; + font-size: clamp(9px, 2.5cqmin, 12px); + line-height: 1.1; + text-overflow: ellipsis; + white-space: nowrap; +} + +.osDonutContent { + display: flex; + align-items: center; + justify-content: center; + flex: 1; + min-height: 0; + max-width: 100%; + overflow: visible; +} + +.osDonutContentWithLegend { + justify-content: space-evenly; + gap: clamp(8px, 3cqi, 16px); +} + +.osDonut { + display: flex; + align-items: center; + justify-content: center; + flex: 0 1 auto; + min-width: 0; + max-width: 100%; + overflow: visible; +} + +.osLegend { + display: flex; + flex: 1 1 0; + flex-direction: column; + gap: 4px; + min-width: 0; + max-width: 50%; +} + +.osLegendItem { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; + font-size: clamp(9px, 2.2cqmin, 12px); + line-height: 1.2; +} + +.osLegendSwatch { + width: 8px; + height: 8px; + flex: 0 0 auto; + border-radius: var(--mantine-radius-xl); + background-color: var(--os-color); +} + +.osLegendLabel { + min-width: 0; + flex: 1 1 auto; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.osLegendValue { + flex: 0 0 auto; +} + +.footer { + flex-shrink: 0; + max-width: 100%; + overflow: hidden; + font-size: clamp(9px, 2.2cqmin, 12px); + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.compactFooter { + font-size: clamp(8px, 2.2cqmin, 10px); +} diff --git a/packages/widgets/src/patchmon/component.tsx b/packages/widgets/src/patchmon/component.tsx new file mode 100644 index 000000000..f1dae3e29 --- /dev/null +++ b/packages/widgets/src/patchmon/component.tsx @@ -0,0 +1,458 @@ +"use client"; + +import type { CSSProperties } from "react"; +import { RingProgress, ScrollArea, Text, Tooltip } from "@mantine/core"; +import { + IconCircleCheck, + IconClockCheck, + IconDatabase, + IconPackages, + IconRefreshAlert, + IconServer, + IconShieldExclamation, + IconShieldX, +} from "@tabler/icons-react"; +import dayjs from "dayjs"; +import relativeTime from "dayjs/plugin/relativeTime"; + +import { clientApi } from "@homarr/api/client"; +import { useScopedI18n } from "@homarr/translation/client"; +import type { TablerIcon } from "@homarr/ui"; + +import { WidgetEmptyState } from "../common/empty-state"; +import type { WidgetComponentProps } from "../definition"; +import classes from "./component.module.css"; +import { getGridCols, shouldShowComplianceHeroText } from "./layout-utils"; +import { OsDistributionSection } from "./os-distribution-section"; +import type { PatchMonStatKey } from "./stat-colors"; +import { resolveStatColor, severityToIconColor, severityToMantineBgColor, severityToMantineColor } from "./stat-colors"; + +dayjs.extend(relativeTime); + +const statVisibilityByOption = { + showTotalHosts: "totalHosts", + showHostsNeedingUpdates: "hostsNeedingUpdates", + showSecurityUpdates: "securityUpdates", + showUpToDateHosts: "upToDateHosts", + showHostsWithSecurityUpdates: "hostsWithSecurityUpdates", + showRecentUpdates24h: "recentUpdates24h", + showTotalOutdatedPackages: "totalOutdatedPackages", + showTotalRepos: "totalRepos", +} as const; + +const statIcons: Record = { + totalHosts: IconServer, + hostsNeedingUpdates: IconRefreshAlert, + securityUpdates: IconShieldExclamation, + upToDateHosts: IconCircleCheck, + hostsWithSecurityUpdates: IconShieldX, + recentUpdates24h: IconClockCheck, + totalOutdatedPackages: IconPackages, + totalRepos: IconDatabase, +}; + +const iconSizeByWidth = [ + { minWidth: 320, size: 22 }, + { minWidth: 200, size: 18 }, + { minWidth: 0, size: 16 }, +] as const; + +const ringSizeByWidth = [ + { minWidth: 400, size: 88 }, + { minWidth: 320, size: 72 }, + { minWidth: 220, size: 56 }, + { minWidth: 0, size: 40 }, +] as const; + +type LayoutMode = "mini" | "compact" | "comfortable" | "spacious"; +type CompactPrimaryContent = "hero" | "os" | "stats" | null; + +const getLayoutMode = (width: number, height: number): LayoutMode => { + const minDim = Math.min(width, height); + if (minDim < 140) return "mini"; + if (minDim < 220) return "compact"; + if (width >= 380 && height >= 350) return "spacious"; + return "comfortable"; +}; + +export default function PatchMonWidget({ integrationIds, options, width, height }: WidgetComponentProps<"patchmon">) { + const t = useScopedI18n("widget.patchmon"); + const integrationId = integrationIds[0] ?? ""; + const { data: stats } = clientApi.widget.patchmon.getStats.useQuery({ integrationId }, { staleTime: 60 * 1000 }); + + if (!stats) return ; + + const statValues: Record = { + totalHosts: stats.totalHosts, + hostsNeedingUpdates: stats.hostsNeedingUpdates, + securityUpdates: stats.securityUpdates, + upToDateHosts: stats.upToDateHosts, + hostsWithSecurityUpdates: stats.hostsWithSecurityUpdates, + recentUpdates24h: stats.recentUpdates24h, + totalOutdatedPackages: stats.totalOutdatedPackages, + totalRepos: stats.totalRepos, + }; + + const visibleStatKeys = Object.entries(statVisibilityByOption) + .filter(([optionKey]) => options[optionKey as keyof typeof options]) + .map(([, statKey]) => statKey); + + const colorContext = { totalHosts: stats.totalHosts }; + const layout = getLayoutMode(width, height); + const isLandscape = width / height > 2.5; + const isTinySquare = width < 160 && height < 160; + const isShortLandscape = width >= 160 && height < 160; + const isCompactSurface = isTinySquare || isShortLandscape; + const isNarrow = width < 180; + const osDisplayMode = options.osDisplayMode as "bars" | "donut"; + const showOsData = options.showOsDistribution && stats.osDistribution.length > 0; + + const showIcons = layout !== "mini"; + const showLabels = layout !== "mini"; + const showHero = options.showComplianceHero && (!isLandscape || isCompactSurface) && width >= 90 && height >= 90; + const showHeroText = showHero && shouldShowComplianceHeroText(width); + const showHeroRingOnly = showHero && !showHeroText; + const compactPrimaryContent = isCompactSurface + ? getCompactPrimaryContent({ + showHero, + showOsDistribution: showOsData, + visibleStatKeys, + }) + : null; + const showStats = visibleStatKeys.length > 0 && !isCompactSurface; + const showOsSection = showOsData && !isCompactSurface && height > 200; + const showCompactFooter = isCompactSurface && height >= 110; + const showFooter = isCompactSurface ? showCompactFooter : !isLandscape && height > 220; + const showOsLegend = osDisplayMode === "donut" && width >= 380 && height >= 300; + + const hasContent = compactPrimaryContent !== null || showStats || showOsSection || showHero; + + const gridCols = isLandscape + ? Math.min(visibleStatKeys.length, 4) + : Math.min(getGridCols(width), visibleStatKeys.length || 1); + const iconSize = getIconSize(width); + const ringSize = getRingSize(width, height, showHeroRingOnly); + + const compliancePercent = stats.totalHosts > 0 ? Math.round((stats.upToDateHosts / stats.totalHosts) * 100) : 100; + const complianceSeverity = resolveStatColor("upToDateHosts", stats.upToDateHosts, colorContext, options); + const complianceColor = complianceSeverity === "neutral" ? "blue" : complianceSeverity; + + const timeAgo = dayjs(stats.lastUpdated).fromNow(); + const compactTimeAgo = getCompactTimeAgo(stats.lastUpdated); + const footerText = isNarrow ? compactTimeAgo : t("lastUpdated", { time: timeAgo }); + + if (isCompactSurface) { + const compactContentHeight = showCompactFooter ? height - 18 : height; + const compactStatCols = Math.max(1, Math.floor(width / 120)); + const compactStatKeys = visibleStatKeys.slice(0, Math.min(visibleStatKeys.length, compactStatCols * 2)); + const compactContent = + compactPrimaryContent === "hero" ? ( + + ) : compactPrimaryContent === "os" ? ( +
+ +
+ ) : compactPrimaryContent === "stats" ? ( + = 180 || height >= 140} + getLabel={t} + /> + ) : null; + + if (!compactContent) { + return ( +
+
+ + — + +
+
+ ); + } + + return ( +
+ {compactContent} + {showCompactFooter && ( + + {footerText} + + )} +
+ ); + } + + if (!hasContent) { + return ( +
+
+ + — + +
+
+ ); + } + + const statGridContent = showStats && ( +
+
+ {visibleStatKeys.map((statKey) => { + const severity = resolveStatColor(statKey, statValues[statKey], colorContext, options); + const Icon = statIcons[statKey]; + + const tile = ( +
+ {showIcons && ( + + )} + + + {statValues[statKey]} + + + {showLabels && {t(statKey)}} +
+ ); + + if (!showLabels) { + return ( + + {tile} + + ); + } + + return tile; + })} +
+
+ ); + + const osContent = showOsSection && ( +
0 ? classes.osSectionWithStats : ""}`}> + +
+ ); + + const needsScroll = layout === "mini" || layout === "compact"; + + return ( +
+ {showHero && ( + + )} + + {(statGridContent || osContent) && + (needsScroll ? ( + +
+ {statGridContent} + {osContent} +
+
+ ) : ( +
+ {statGridContent} + {osContent} +
+ ))} + + {showFooter && ( + + {footerText} + + )} +
+ ); +} + +function ComplianceHero({ + compliancePercent, + complianceColor, + upToDateHosts, + totalHosts, + ringSize, + showText, + expanded = false, + compact = false, + getLabel, +}: { + compliancePercent: number; + complianceColor: string; + upToDateHosts: number; + totalHosts: number; + ringSize: number; + showText: boolean; + expanded?: boolean; + compact?: boolean; + getLabel: (key: "compliance" | "complianceRatio", params?: { upToDate: number; total: number }) => string; +}) { + const heroClasses = [ + compact && classes.compactPrimary, + classes.hero, + expanded && classes.heroExpanded, + showText ? classes.heroWithText : classes.heroRingOnly, + ] + .filter(Boolean) + .join(" "); + + return ( +
+ {showText && ( +
+ {getLabel("compliance")} + + {getLabel("complianceRatio", { upToDate: upToDateHosts, total: totalHosts })} + +
+ )} + + {compliancePercent}% + + } + /> +
+ ); +} + +function getIconSize(width: number): number { + const match = iconSizeByWidth.find(({ minWidth }) => width >= minWidth); + return match?.size ?? 16; +} + +function getRingSize(width: number, height: number, compact: boolean): number { + if (compact) { + return Math.max(36, Math.min(64, width - 28, height - 28)); + } + + const match = ringSizeByWidth.find(({ minWidth }) => width >= minWidth); + return match?.size ?? 44; +} + +function getCompactPrimaryContent({ + showHero, + showOsDistribution, + visibleStatKeys, +}: { + showHero: boolean; + showOsDistribution: boolean; + visibleStatKeys: PatchMonStatKey[]; +}): CompactPrimaryContent { + if (showHero) return "hero"; + if (showOsDistribution) return "os"; + return visibleStatKeys.length > 0 ? "stats" : null; +} + +function getCompactTimeAgo(lastUpdated: string): string { + const minutes = Math.max(0, dayjs().diff(dayjs(lastUpdated), "minute")); + + if (minutes < 1) return "now"; + if (minutes < 60) return `${minutes}m ago`; + + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + + const days = Math.floor(hours / 24); + return `${days}d ago`; +} + +function CompactStatGrid({ + statKeys, + statValues, + colorContext, + options, + columns, + showIcon, + getLabel, +}: { + statKeys: PatchMonStatKey[]; + statValues: Record; + colorContext: { totalHosts: number }; + options: WidgetComponentProps<"patchmon">["options"]; + columns: number; + showIcon: boolean; + getLabel: (key: PatchMonStatKey) => string; +}) { + return ( +
+ {statKeys.map((statKey) => { + const severity = resolveStatColor(statKey, statValues[statKey], colorContext, options); + const Icon = statIcons[statKey]; + const label = getLabel(statKey); + + return ( + +
+ {showIcon && ( + + )} + + + {statValues[statKey]} + + + {label} +
+
+ ); + })} +
+ ); +} diff --git a/packages/widgets/src/patchmon/index.ts b/packages/widgets/src/patchmon/index.ts new file mode 100644 index 000000000..6f97be83f --- /dev/null +++ b/packages/widgets/src/patchmon/index.ts @@ -0,0 +1,234 @@ +import { IconServerOff, IconShieldCheck } from "@tabler/icons-react"; +import { z } from "zod/v4"; + +import { createWidgetDefinition } from "../definition"; +import { optionsBuilder } from "../options"; +import { PATCHMON_STAT_COLOR_PRESETS } from "./stat-colors"; +import { patchmonOptionsSuperRefine } from "./threshold-validation"; + +export const { definition, componentLoader } = createWidgetDefinition("patchmon", { + icon: IconShieldCheck, + supportedIntegrations: ["patchmon"], + integrationsRequired: true, + createOptions() { + return optionsBuilder.from( + (factory) => ({ + showComplianceHero: factory.switch({ + defaultValue: true, + withDescription: true, + }), + showTotalHosts: factory.switch({ + defaultValue: true, + withDescription: true, + }), + showHostsNeedingUpdates: factory.switch({ + defaultValue: true, + withDescription: true, + }), + showSecurityUpdates: factory.switch({ + defaultValue: true, + withDescription: true, + }), + showUpToDateHosts: factory.switch({ + defaultValue: false, + withDescription: true, + }), + showHostsWithSecurityUpdates: factory.switch({ + defaultValue: false, + withDescription: true, + }), + showRecentUpdates24h: factory.switch({ + defaultValue: false, + withDescription: true, + }), + showTotalOutdatedPackages: factory.switch({ + defaultValue: false, + withDescription: true, + }), + showTotalRepos: factory.switch({ + defaultValue: false, + withDescription: true, + }), + enableThresholdColors: factory.switch({ + defaultValue: true, + withDescription: true, + }), + useCustomThresholds: factory.switch({ + defaultValue: false, + withDescription: true, + }), + hostsNeedingUpdatesThresholdMode: factory.select({ + defaultValue: PATCHMON_STAT_COLOR_PRESETS.hostsNeedingUpdates.mode, + withDescription: true, + options: [ + { value: "absolute", label: (t) => t("widget.patchmon.option.thresholdMode.option.absolute") }, + { value: "percent", label: (t) => t("widget.patchmon.option.thresholdMode.option.percent") }, + ] as const, + }), + hostsNeedingUpdatesWarningAt: factory.number({ + defaultValue: PATCHMON_STAT_COLOR_PRESETS.hostsNeedingUpdates.warningAt, + withDescription: true, + step: 1, + validate: z.number().int().min(0), + }), + hostsNeedingUpdatesCriticalAt: factory.number({ + defaultValue: PATCHMON_STAT_COLOR_PRESETS.hostsNeedingUpdates.criticalAt, + withDescription: true, + step: 1, + validate: z.number().int().min(0), + }), + securityUpdatesThresholdMode: factory.select({ + defaultValue: PATCHMON_STAT_COLOR_PRESETS.securityUpdates.mode, + withDescription: true, + options: [ + { value: "absolute", label: (t) => t("widget.patchmon.option.thresholdMode.option.absolute") }, + { value: "percent", label: (t) => t("widget.patchmon.option.thresholdMode.option.percent") }, + ] as const, + }), + securityUpdatesWarningAt: factory.number({ + defaultValue: PATCHMON_STAT_COLOR_PRESETS.securityUpdates.warningAt, + withDescription: true, + step: 1, + validate: z.number().int().min(0), + }), + securityUpdatesCriticalAt: factory.number({ + defaultValue: PATCHMON_STAT_COLOR_PRESETS.securityUpdates.criticalAt, + withDescription: true, + step: 1, + validate: z.number().int().min(0), + }), + hostsWithSecurityUpdatesThresholdMode: factory.select({ + defaultValue: PATCHMON_STAT_COLOR_PRESETS.hostsWithSecurityUpdates.mode, + withDescription: true, + options: [ + { value: "absolute", label: (t) => t("widget.patchmon.option.thresholdMode.option.absolute") }, + { value: "percent", label: (t) => t("widget.patchmon.option.thresholdMode.option.percent") }, + ] as const, + }), + hostsWithSecurityUpdatesWarningAt: factory.number({ + defaultValue: PATCHMON_STAT_COLOR_PRESETS.hostsWithSecurityUpdates.warningAt, + withDescription: true, + step: 1, + validate: z.number().int().min(0), + }), + hostsWithSecurityUpdatesCriticalAt: factory.number({ + defaultValue: PATCHMON_STAT_COLOR_PRESETS.hostsWithSecurityUpdates.criticalAt, + withDescription: true, + step: 1, + validate: z.number().int().min(0), + }), + upToDateHostsThresholdMode: factory.select({ + defaultValue: PATCHMON_STAT_COLOR_PRESETS.upToDateHosts.mode, + withDescription: true, + options: [ + { value: "absolute", label: (t) => t("widget.patchmon.option.thresholdMode.option.absolute") }, + { value: "percent", label: (t) => t("widget.patchmon.option.thresholdMode.option.percent") }, + ] as const, + }), + upToDateHostsWarningAt: factory.number({ + defaultValue: PATCHMON_STAT_COLOR_PRESETS.upToDateHosts.warningAt, + withDescription: true, + step: 1, + validate: z.number().int().min(0), + }), + upToDateHostsCriticalAt: factory.number({ + defaultValue: PATCHMON_STAT_COLOR_PRESETS.upToDateHosts.criticalAt, + withDescription: true, + step: 1, + validate: z.number().int().min(0), + }), + totalOutdatedPackagesThresholdMode: factory.select({ + defaultValue: PATCHMON_STAT_COLOR_PRESETS.totalOutdatedPackages.mode, + withDescription: true, + options: [ + { value: "absolute", label: (t) => t("widget.patchmon.option.thresholdMode.option.absolute") }, + { value: "percent", label: (t) => t("widget.patchmon.option.thresholdMode.option.percent") }, + ] as const, + }), + totalOutdatedPackagesWarningAt: factory.number({ + defaultValue: PATCHMON_STAT_COLOR_PRESETS.totalOutdatedPackages.warningAt, + withDescription: true, + step: 1, + validate: z.number().int().min(0), + }), + totalOutdatedPackagesCriticalAt: factory.number({ + defaultValue: PATCHMON_STAT_COLOR_PRESETS.totalOutdatedPackages.criticalAt, + withDescription: true, + step: 1, + validate: z.number().int().min(0), + }), + showOsDistribution: factory.switch({ + defaultValue: false, + withDescription: true, + }), + osDisplayMode: factory.select({ + defaultValue: "bars", + withDescription: true, + options: [ + { value: "bars", label: (t) => t("widget.patchmon.option.osDisplayMode.option.bars") }, + { value: "donut", label: (t) => t("widget.patchmon.option.osDisplayMode.option.donut") }, + ] as const, + }), + osDistributionLimit: factory.select({ + defaultValue: "0", + withDescription: true, + options: [ + { value: "3", label: (t) => t("widget.patchmon.option.osDistributionLimit.option.3") }, + { value: "5", label: (t) => t("widget.patchmon.option.osDistributionLimit.option.5") }, + { value: "10", label: (t) => t("widget.patchmon.option.osDistributionLimit.option.10") }, + { value: "0", label: (t) => t("widget.patchmon.option.osDistributionLimit.option.all") }, + ] as const, + }), + showOsVersion: factory.switch({ + defaultValue: true, + withDescription: true, + }), + }), + { + useCustomThresholds: { + shouldHide: (options) => !options.enableThresholdColors, + }, + hostsNeedingUpdatesThresholdMode: { shouldHide: (options) => !options.enableThresholdColors || !options.useCustomThresholds }, + hostsNeedingUpdatesWarningAt: { shouldHide: (options) => !options.enableThresholdColors || !options.useCustomThresholds }, + hostsNeedingUpdatesCriticalAt: { shouldHide: (options) => !options.enableThresholdColors || !options.useCustomThresholds }, + securityUpdatesThresholdMode: { shouldHide: (options) => !options.enableThresholdColors || !options.useCustomThresholds }, + securityUpdatesWarningAt: { shouldHide: (options) => !options.enableThresholdColors || !options.useCustomThresholds }, + securityUpdatesCriticalAt: { shouldHide: (options) => !options.enableThresholdColors || !options.useCustomThresholds }, + hostsWithSecurityUpdatesThresholdMode: { shouldHide: (options) => !options.enableThresholdColors || !options.useCustomThresholds }, + hostsWithSecurityUpdatesWarningAt: { shouldHide: (options) => !options.enableThresholdColors || !options.useCustomThresholds }, + hostsWithSecurityUpdatesCriticalAt: { shouldHide: (options) => !options.enableThresholdColors || !options.useCustomThresholds }, + upToDateHostsThresholdMode: { shouldHide: (options) => !options.enableThresholdColors || !options.useCustomThresholds }, + upToDateHostsWarningAt: { shouldHide: (options) => !options.enableThresholdColors || !options.useCustomThresholds }, + upToDateHostsCriticalAt: { shouldHide: (options) => !options.enableThresholdColors || !options.useCustomThresholds }, + totalOutdatedPackagesThresholdMode: { + shouldHide: (options) => + !options.enableThresholdColors || !options.useCustomThresholds || !options.showTotalOutdatedPackages, + }, + totalOutdatedPackagesWarningAt: { + shouldHide: (options) => + !options.enableThresholdColors || !options.useCustomThresholds || !options.showTotalOutdatedPackages, + }, + totalOutdatedPackagesCriticalAt: { + shouldHide: (options) => + !options.enableThresholdColors || !options.useCustomThresholds || !options.showTotalOutdatedPackages, + }, + osDisplayMode: { + shouldHide: (options) => !options.showOsDistribution, + }, + osDistributionLimit: { + shouldHide: (options) => !options.showOsDistribution, + }, + showOsVersion: { + shouldHide: (options) => !options.showOsDistribution, + }, + }, + patchmonOptionsSuperRefine, + ); + }, + errors: { + INTERNAL_SERVER_ERROR: { + icon: IconServerOff, + message: (t) => t("widget.patchmon.error.internalServerError"), + }, + }, +}).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/patchmon/layout-utils.ts b/packages/widgets/src/patchmon/layout-utils.ts new file mode 100644 index 000000000..dd8ca1282 --- /dev/null +++ b/packages/widgets/src/patchmon/layout-utils.ts @@ -0,0 +1,15 @@ +const gridColsByWidth = [ + { minWidth: 500, cols: 4 }, + { minWidth: 380, cols: 3 }, + { minWidth: 220, cols: 2 }, + { minWidth: 0, cols: 1 }, +] as const; + +export function getGridCols(width: number): number { + const match = gridColsByWidth.find(({ minWidth }) => width >= minWidth); + return match?.cols ?? 1; +} + +export function shouldShowComplianceHeroText(width: number): boolean { + return getGridCols(width) >= 2; +} diff --git a/packages/widgets/src/patchmon/os-distribution-section.tsx b/packages/widgets/src/patchmon/os-distribution-section.tsx new file mode 100644 index 000000000..cb21cb203 --- /dev/null +++ b/packages/widgets/src/patchmon/os-distribution-section.tsx @@ -0,0 +1,167 @@ +"use client"; + +import type { CSSProperties } from "react"; +import { DonutChart } from "@mantine/charts"; +import { Group, Progress, ScrollArea, Stack, Text } from "@mantine/core"; + +import type { PatchMonOsDistributionEntry } from "@homarr/integrations"; +import { useScopedI18n } from "@homarr/translation/client"; + +import classes from "./component.module.css"; + +const OS_COLORS = [ + "blue.6", + "orange.6", + "green.6", + "grape.6", + "cyan.6", + "pink.6", + "yellow.6", + "teal.6", +] as const; + +interface OsDistributionSectionProps { + entries: PatchMonOsDistributionEntry[]; + limit: number; + showOsVersion: boolean; + displayMode: "bars" | "donut"; + width: number; + height: number; + showLegend: boolean; + compact?: boolean; +} + +export const formatOsLabel = (entry: PatchMonOsDistributionEntry, showVersion: boolean) => + showVersion && entry.osVersion ? `${entry.name} ${entry.osVersion}` : entry.name; + +const getMantineColorVariable = (color: string) => `var(--mantine-color-${color.replace(".", "-")})`; + +export function OsDistributionSection({ + entries, + limit, + showOsVersion, + displayMode, + width, + height, + showLegend, + compact = false, +}: OsDistributionSectionProps) { + const t = useScopedI18n("widget.patchmon"); + + if (entries.length === 0) { + return null; + } + + const visibleEntries = limit > 0 ? entries.slice(0, limit) : entries; + + if (displayMode === "donut") { + const chartData = visibleEntries.map((entry, i) => ({ + name: formatOsLabel(entry, showOsVersion), + value: entry.count, + color: OS_COLORS[i % OS_COLORS.length] as string, + })); + const donutSize = compact + ? Math.max(36, Math.min(width - 18, height - 10, 96)) + : Math.max(48, Math.min(width - 24, height * 0.55, showLegend ? 140 : 180)); + + return ( + + {!compact && ( + + {t("osDistribution")} + + )} +
+
+ +
+ {showLegend && ( +
+ {chartData.map((entry) => ( +
+ + + {entry.name} + + + {entry.value} + +
+ ))} +
+ )} +
+
+ ); + } + + const maxCount = Math.max(...visibleEntries.map((entry) => entry.count), 1); + + if (compact) { + const maxRows = Math.max(2, Math.min(visibleEntries.length, Math.floor(height / 28), width < 160 ? 2 : 4)); + const compactEntries = visibleEntries.slice(0, maxRows); + if (compactEntries.length === 0) return null; + + return ( +
+ {compactEntries.map((entry, i) => ( +
+ + {formatOsLabel(entry, showOsVersion)} + + + + + {entry.count} + + +
+ ))} +
+ ); + } + + return ( + + + {t("osDistribution")} + + + + {visibleEntries.map((entry, i) => { + const barValue = (entry.count / maxCount) * 100; + const barColor = OS_COLORS[i % OS_COLORS.length] as string; + return ( +
+ + {formatOsLabel(entry, showOsVersion)} + + + + + {entry.count} + + +
+ ); + })} +
+
+
+ ); +} diff --git a/packages/widgets/src/patchmon/stat-colors.ts b/packages/widgets/src/patchmon/stat-colors.ts new file mode 100644 index 000000000..7ecd035ac --- /dev/null +++ b/packages/widgets/src/patchmon/stat-colors.ts @@ -0,0 +1,215 @@ +import type { WidgetComponentProps } from "../definition"; +import { clampPercentThreshold } from "./threshold-validation"; + +export type PatchMonStatKey = + | "totalHosts" + | "hostsNeedingUpdates" + | "securityUpdates" + | "upToDateHosts" + | "hostsWithSecurityUpdates" + | "recentUpdates24h" + | "totalOutdatedPackages" + | "totalRepos"; + +export type ColorablePatchMonStatKey = Extract< + PatchMonStatKey, + | "hostsNeedingUpdates" + | "securityUpdates" + | "hostsWithSecurityUpdates" + | "upToDateHosts" + | "totalOutdatedPackages" +>; + +export type StatSeverity = "green" | "yellow" | "red" | "neutral"; +export type ThresholdMode = "absolute" | "percent"; + +export interface StatColorContext { + totalHosts: number; +} + +export interface StatThresholdRule { + direction: "lowerIsBetter" | "higherIsBetter"; + mode: ThresholdMode; + warningAt: number; + criticalAt: number; + skipYellow?: boolean; +} + +type PatchMonColorOptions = WidgetComponentProps<"patchmon">["options"]; + +export const COLORABLE_PATCHMON_STATS = [ + "hostsNeedingUpdates", + "securityUpdates", + "hostsWithSecurityUpdates", + "upToDateHosts", + "totalOutdatedPackages", +] as const satisfies readonly ColorablePatchMonStatKey[]; + +export const NEUTRAL_PATCHMON_STATS = ["totalHosts", "totalRepos", "recentUpdates24h"] as const; + +export const PATCHMON_STAT_COLOR_PRESETS: Record = { + hostsNeedingUpdates: { + direction: "lowerIsBetter", + mode: "absolute", + warningAt: 1, + criticalAt: 5, + }, + securityUpdates: { + direction: "lowerIsBetter", + mode: "absolute", + warningAt: 1, + criticalAt: 10, + }, + hostsWithSecurityUpdates: { + direction: "lowerIsBetter", + mode: "absolute", + warningAt: 1, + criticalAt: 1, + skipYellow: true, + }, + upToDateHosts: { + direction: "higherIsBetter", + mode: "percent", + warningAt: 90, + criticalAt: 90, + }, + totalOutdatedPackages: { + direction: "lowerIsBetter", + mode: "absolute", + warningAt: 1, + criticalAt: 50, + }, +}; + +const thresholdModeOptionKey = { + hostsNeedingUpdates: "hostsNeedingUpdatesThresholdMode", + securityUpdates: "securityUpdatesThresholdMode", + hostsWithSecurityUpdates: "hostsWithSecurityUpdatesThresholdMode", + upToDateHosts: "upToDateHostsThresholdMode", + totalOutdatedPackages: "totalOutdatedPackagesThresholdMode", +} as const; + +const warningAtOptionKey = { + hostsNeedingUpdates: "hostsNeedingUpdatesWarningAt", + securityUpdates: "securityUpdatesWarningAt", + hostsWithSecurityUpdates: "hostsWithSecurityUpdatesWarningAt", + upToDateHosts: "upToDateHostsWarningAt", + totalOutdatedPackages: "totalOutdatedPackagesWarningAt", +} as const; + +const criticalAtOptionKey = { + hostsNeedingUpdates: "hostsNeedingUpdatesCriticalAt", + securityUpdates: "securityUpdatesCriticalAt", + hostsWithSecurityUpdates: "hostsWithSecurityUpdatesCriticalAt", + upToDateHosts: "upToDateHostsCriticalAt", + totalOutdatedPackages: "totalOutdatedPackagesCriticalAt", +} as const; + +const isColorableStat = (statKey: PatchMonStatKey): statKey is ColorablePatchMonStatKey => + COLORABLE_PATCHMON_STATS.includes(statKey as ColorablePatchMonStatKey); + +const getComparableValue = ( + value: number, + mode: ThresholdMode, + context: StatColorContext, + direction: StatThresholdRule["direction"], +): number => { + if (mode === "absolute") { + return value; + } + + if (context.totalHosts <= 0) { + if (direction === "higherIsBetter") { + return 100; + } + + return value > 0 ? 100 : 0; + } + + return (value / context.totalHosts) * 100; +}; + +const resolveLowerIsBetter = (comparable: number, rule: StatThresholdRule): StatSeverity => { + if (comparable < rule.warningAt) { + return "green"; + } + + if (rule.skipYellow || comparable >= rule.criticalAt) { + return "red"; + } + + return "yellow"; +}; + +const resolveHigherIsBetter = ( + value: number, + comparable: number, + context: StatColorContext, + rule: StatThresholdRule, +): StatSeverity => { + if (context.totalHosts > 0 && value >= context.totalHosts) { + return "green"; + } + + if (comparable >= rule.warningAt) { + return "green"; + } + + if (rule.skipYellow || comparable < rule.criticalAt) { + return "red"; + } + + return "yellow"; +}; + +const getThresholdRule = (statKey: ColorablePatchMonStatKey, options: PatchMonColorOptions): StatThresholdRule => { + if (!options.useCustomThresholds) { + return PATCHMON_STAT_COLOR_PRESETS[statKey]; + } + + const mode = options[thresholdModeOptionKey[statKey]] as ThresholdMode; + const warningAt = clampPercentThreshold(options[warningAtOptionKey[statKey]] as number, mode); + const criticalAt = clampPercentThreshold(options[criticalAtOptionKey[statKey]] as number, mode); + const preset = PATCHMON_STAT_COLOR_PRESETS[statKey]; + + return { + direction: preset.direction, + mode, + warningAt, + criticalAt, + skipYellow: preset.skipYellow && warningAt === criticalAt, + }; +}; + +export const resolveStatColor = ( + statKey: PatchMonStatKey, + value: number, + context: StatColorContext, + options: PatchMonColorOptions, +): StatSeverity => { + if (!options.enableThresholdColors || !isColorableStat(statKey)) { + return "neutral"; + } + + const rule = getThresholdRule(statKey, options); + const comparable = getComparableValue(value, rule.mode, context, rule.direction); + + if (rule.direction === "higherIsBetter") { + return resolveHigherIsBetter(value, comparable, context, rule); + } + + return resolveLowerIsBetter(comparable, rule); +}; + +export const severityToMantineColor = (severity: StatSeverity): string => + severity === "neutral" ? "var(--mantine-primary-color-filled)" : severity; + +export const severityToMantineBgColor = (severity: StatSeverity): string => { + if (severity === "neutral") return "light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6))"; + return `var(--mantine-color-${severity}-light)`; +}; + +export const severityToIconColor = (severity: StatSeverity): string => { + if (severity === "neutral") return "var(--mantine-color-dimmed)"; + return `var(--mantine-color-${severity}-6)`; +}; diff --git a/packages/widgets/src/patchmon/test/layout-utils.spec.ts b/packages/widgets/src/patchmon/test/layout-utils.spec.ts new file mode 100644 index 000000000..b2a486c67 --- /dev/null +++ b/packages/widgets/src/patchmon/test/layout-utils.spec.ts @@ -0,0 +1,36 @@ +import { describe, expect, test } from "vitest"; + +import { getGridCols, shouldShowComplianceHeroText } from "../layout-utils"; + +describe("getGridCols", () => { + test("returns 1 column for narrow widths", () => { + expect(getGridCols(128)).toBe(1); + expect(getGridCols(219)).toBe(1); + }); + + test("returns 2 columns at 220px breakpoint", () => { + expect(getGridCols(220)).toBe(2); + expect(getGridCols(256)).toBe(2); + }); + + test("returns 3 columns at 380px breakpoint", () => { + expect(getGridCols(380)).toBe(3); + }); + + test("returns 4 columns at 500px breakpoint", () => { + expect(getGridCols(500)).toBe(4); + }); +}); + +describe("shouldShowComplianceHeroText", () => { + test("returns false for 1-column-wide layouts", () => { + expect(shouldShowComplianceHeroText(128)).toBe(false); + expect(shouldShowComplianceHeroText(219)).toBe(false); + }); + + test("returns true for 2+-column-wide layouts", () => { + expect(shouldShowComplianceHeroText(220)).toBe(true); + expect(shouldShowComplianceHeroText(256)).toBe(true); + expect(shouldShowComplianceHeroText(384)).toBe(true); + }); +}); diff --git a/packages/widgets/src/patchmon/test/stat-colors.spec.ts b/packages/widgets/src/patchmon/test/stat-colors.spec.ts new file mode 100644 index 000000000..12596f499 --- /dev/null +++ b/packages/widgets/src/patchmon/test/stat-colors.spec.ts @@ -0,0 +1,171 @@ +import { describe, expect, test } from "vitest"; + +import type { WidgetComponentProps } from "../../definition"; +import { resolveStatColor } from "../stat-colors"; + +const defaultOptions = { + enableThresholdColors: true, + useCustomThresholds: false, + showTotalHosts: true, + showHostsNeedingUpdates: true, + showSecurityUpdates: true, + showUpToDateHosts: false, + showHostsWithSecurityUpdates: false, + showRecentUpdates24h: false, + showTotalOutdatedPackages: false, + showTotalRepos: false, + showOsDistribution: false, + osDistributionLimit: "0", + showOsVersion: true, + hostsNeedingUpdatesThresholdMode: "absolute", + hostsNeedingUpdatesWarningAt: 1, + hostsNeedingUpdatesCriticalAt: 5, + securityUpdatesThresholdMode: "absolute", + securityUpdatesWarningAt: 1, + securityUpdatesCriticalAt: 10, + hostsWithSecurityUpdatesThresholdMode: "absolute", + hostsWithSecurityUpdatesWarningAt: 1, + hostsWithSecurityUpdatesCriticalAt: 1, + upToDateHostsThresholdMode: "percent", + upToDateHostsWarningAt: 90, + upToDateHostsCriticalAt: 90, + totalOutdatedPackagesThresholdMode: "absolute", + totalOutdatedPackagesWarningAt: 1, + totalOutdatedPackagesCriticalAt: 50, + showComplianceHero: true, + osDisplayMode: "bars", +} satisfies WidgetComponentProps<"patchmon">["options"]; + +const context = { totalHosts: 100 }; + +describe("resolveStatColor presets", () => { + test("hostsNeedingUpdates uses green, yellow, and red boundaries", () => { + expect(resolveStatColor("hostsNeedingUpdates", 0, context, defaultOptions)).toBe("green"); + expect(resolveStatColor("hostsNeedingUpdates", 1, context, defaultOptions)).toBe("yellow"); + expect(resolveStatColor("hostsNeedingUpdates", 4, context, defaultOptions)).toBe("yellow"); + expect(resolveStatColor("hostsNeedingUpdates", 5, context, defaultOptions)).toBe("red"); + }); + + test("securityUpdates uses green and yellow for low counts", () => { + expect(resolveStatColor("securityUpdates", 0, context, defaultOptions)).toBe("green"); + expect(resolveStatColor("securityUpdates", 1, context, defaultOptions)).toBe("yellow"); + expect(resolveStatColor("securityUpdates", 10, context, defaultOptions)).toBe("red"); + }); + + test("hostsWithSecurityUpdates skips yellow", () => { + expect(resolveStatColor("hostsWithSecurityUpdates", 0, context, defaultOptions)).toBe("green"); + expect(resolveStatColor("hostsWithSecurityUpdates", 1, context, defaultOptions)).toBe("red"); + }); + + test("upToDateHosts uses percent of total hosts", () => { + expect(resolveStatColor("upToDateHosts", 100, context, defaultOptions)).toBe("green"); + expect(resolveStatColor("upToDateHosts", 95, context, defaultOptions)).toBe("green"); + expect(resolveStatColor("upToDateHosts", 90, context, defaultOptions)).toBe("green"); + expect(resolveStatColor("upToDateHosts", 80, context, defaultOptions)).toBe("red"); + }); + + test("neutral stats ignore threshold coloring", () => { + expect(resolveStatColor("totalHosts", 100, context, defaultOptions)).toBe("neutral"); + expect(resolveStatColor("totalRepos", 12, context, defaultOptions)).toBe("neutral"); + expect(resolveStatColor("recentUpdates24h", 34, context, defaultOptions)).toBe("neutral"); + }); +}); + +describe("resolveStatColor options", () => { + test("returns neutral when threshold colors are disabled", () => { + expect( + resolveStatColor("hostsNeedingUpdates", 10, context, { + ...defaultOptions, + enableThresholdColors: false, + }), + ).toBe("neutral"); + }); + + test("supports custom absolute thresholds", () => { + expect( + resolveStatColor("hostsNeedingUpdates", 4, context, { + ...defaultOptions, + useCustomThresholds: true, + hostsNeedingUpdatesWarningAt: 5, + hostsNeedingUpdatesCriticalAt: 10, + }), + ).toBe("green"); + }); + + test("supports custom percent thresholds for lower-is-better stats", () => { + expect( + resolveStatColor("hostsNeedingUpdates", 5, context, { + ...defaultOptions, + useCustomThresholds: true, + hostsNeedingUpdatesThresholdMode: "percent", + hostsNeedingUpdatesWarningAt: 10, + hostsNeedingUpdatesCriticalAt: 20, + }), + ).toBe("green"); + + expect( + resolveStatColor("hostsNeedingUpdates", 15, context, { + ...defaultOptions, + useCustomThresholds: true, + hostsNeedingUpdatesThresholdMode: "percent", + hostsNeedingUpdatesWarningAt: 10, + hostsNeedingUpdatesCriticalAt: 20, + }), + ).toBe("yellow"); + }); + + test("handles zero total hosts for percent-based stats", () => { + expect(resolveStatColor("upToDateHosts", 0, { totalHosts: 0 }, defaultOptions)).toBe("green"); + expect( + resolveStatColor("hostsNeedingUpdates", 1, { totalHosts: 0 }, { + ...defaultOptions, + useCustomThresholds: true, + hostsNeedingUpdatesThresholdMode: "percent", + }), + ).toBe("red"); + }); + + test("clamps invalid percent thresholds when resolving colors", () => { + expect( + resolveStatColor("hostsNeedingUpdates", 50, context, { + ...defaultOptions, + useCustomThresholds: true, + hostsNeedingUpdatesThresholdMode: "percent", + hostsNeedingUpdatesWarningAt: 40, + hostsNeedingUpdatesCriticalAt: 200, + }), + ).toBe("yellow"); + }); + + test("supports custom absolute thresholds for higher-is-better stats", () => { + expect( + resolveStatColor("upToDateHosts", 85, context, { + ...defaultOptions, + useCustomThresholds: true, + upToDateHostsThresholdMode: "absolute", + upToDateHostsWarningAt: 80, + upToDateHostsCriticalAt: 60, + }), + ).toBe("green"); + + expect( + resolveStatColor("upToDateHosts", 70, context, { + ...defaultOptions, + useCustomThresholds: true, + upToDateHostsThresholdMode: "absolute", + upToDateHostsWarningAt: 80, + upToDateHostsCriticalAt: 60, + }), + ).toBe("yellow"); + + expect( + resolveStatColor("upToDateHosts", 50, context, { + ...defaultOptions, + useCustomThresholds: true, + upToDateHostsThresholdMode: "absolute", + upToDateHostsWarningAt: 80, + upToDateHostsCriticalAt: 60, + }), + ).toBe("red"); + }); +}); diff --git a/packages/widgets/src/patchmon/test/threshold-validation.spec.ts b/packages/widgets/src/patchmon/test/threshold-validation.spec.ts new file mode 100644 index 000000000..00dafba3b --- /dev/null +++ b/packages/widgets/src/patchmon/test/threshold-validation.spec.ts @@ -0,0 +1,97 @@ +import { describe, expect, test } from "vitest"; +import { z } from "zod/v4"; + +import { clampPercentThreshold, patchmonOptionsSuperRefine } from "../threshold-validation"; + +const baseOptions = { + enableThresholdColors: true, + useCustomThresholds: true, + hostsNeedingUpdatesThresholdMode: "percent", + hostsNeedingUpdatesWarningAt: 10, + hostsNeedingUpdatesCriticalAt: 20, + securityUpdatesThresholdMode: "absolute", + securityUpdatesWarningAt: 1, + securityUpdatesCriticalAt: 10, + hostsWithSecurityUpdatesThresholdMode: "absolute", + hostsWithSecurityUpdatesWarningAt: 1, + hostsWithSecurityUpdatesCriticalAt: 1, + upToDateHostsThresholdMode: "percent", + upToDateHostsWarningAt: 90, + upToDateHostsCriticalAt: 90, + totalOutdatedPackagesThresholdMode: "absolute", + totalOutdatedPackagesWarningAt: 1, + totalOutdatedPackagesCriticalAt: 50, +}; + +const optionsSchema = z.object({ + hostsNeedingUpdatesWarningAt: z.number().int().min(0), + hostsNeedingUpdatesCriticalAt: z.number().int().min(0), + securityUpdatesWarningAt: z.number().int().min(0), + securityUpdatesCriticalAt: z.number().int().min(0), + hostsWithSecurityUpdatesWarningAt: z.number().int().min(0), + hostsWithSecurityUpdatesCriticalAt: z.number().int().min(0), + upToDateHostsWarningAt: z.number().int().min(0), + upToDateHostsCriticalAt: z.number().int().min(0), + totalOutdatedPackagesWarningAt: z.number().int().min(0), + totalOutdatedPackagesCriticalAt: z.number().int().min(0), + enableThresholdColors: z.boolean(), + useCustomThresholds: z.boolean(), + hostsNeedingUpdatesThresholdMode: z.enum(["absolute", "percent"]), + securityUpdatesThresholdMode: z.enum(["absolute", "percent"]), + hostsWithSecurityUpdatesThresholdMode: z.enum(["absolute", "percent"]), + upToDateHostsThresholdMode: z.enum(["absolute", "percent"]), + totalOutdatedPackagesThresholdMode: z.enum(["absolute", "percent"]), +}).superRefine(patchmonOptionsSuperRefine); + +describe("patchmonOptionsSuperRefine", () => { + test("accepts percent thresholds within 0-100", () => { + const result = optionsSchema.safeParse(baseOptions); + + expect(result.success).toBe(true); + }); + + test("rejects percent thresholds above 100", () => { + const result = optionsSchema.safeParse({ + ...baseOptions, + hostsNeedingUpdatesWarningAt: 101, + }); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues.some((issue) => issue.path.includes("hostsNeedingUpdatesWarningAt"))).toBe(true); + } + }); + + test("allows absolute thresholds above 100", () => { + const result = optionsSchema.safeParse({ + ...baseOptions, + hostsNeedingUpdatesThresholdMode: "absolute", + hostsNeedingUpdatesWarningAt: 150, + hostsNeedingUpdatesCriticalAt: 200, + }); + + expect(result.success).toBe(true); + }); + + test("skips validation when custom thresholds are disabled", () => { + const result = optionsSchema.safeParse({ + ...baseOptions, + useCustomThresholds: false, + hostsNeedingUpdatesWarningAt: 500, + }); + + expect(result.success).toBe(true); + }); +}); + +describe("clampPercentThreshold", () => { + test("clamps percent values to 0-100", () => { + expect(clampPercentThreshold(150, "percent")).toBe(100); + expect(clampPercentThreshold(-5, "percent")).toBe(0); + expect(clampPercentThreshold(42.9, "percent")).toBe(42); + }); + + test("leaves absolute values unchanged", () => { + expect(clampPercentThreshold(150, "absolute")).toBe(150); + }); +}); diff --git a/packages/widgets/src/patchmon/threshold-validation.ts b/packages/widgets/src/patchmon/threshold-validation.ts new file mode 100644 index 000000000..965c8f263 --- /dev/null +++ b/packages/widgets/src/patchmon/threshold-validation.ts @@ -0,0 +1,59 @@ +import type { RefinementCtx } from "zod/v4"; +import { z } from "zod/v4"; + +import type { ThresholdMode } from "./stat-colors"; + +const percentThresholdSchema = z.number().int().min(0).max(100); + +export const PATCHMON_THRESHOLD_PAIRS = [ + { + mode: "hostsNeedingUpdatesThresholdMode", + warningAt: "hostsNeedingUpdatesWarningAt", + criticalAt: "hostsNeedingUpdatesCriticalAt", + }, + { + mode: "securityUpdatesThresholdMode", + warningAt: "securityUpdatesWarningAt", + criticalAt: "securityUpdatesCriticalAt", + }, + { + mode: "hostsWithSecurityUpdatesThresholdMode", + warningAt: "hostsWithSecurityUpdatesWarningAt", + criticalAt: "hostsWithSecurityUpdatesCriticalAt", + }, + { + mode: "upToDateHostsThresholdMode", + warningAt: "upToDateHostsWarningAt", + criticalAt: "upToDateHostsCriticalAt", + }, + { + mode: "totalOutdatedPackagesThresholdMode", + warningAt: "totalOutdatedPackagesWarningAt", + criticalAt: "totalOutdatedPackagesCriticalAt", + }, +] as const; + +export const clampPercentThreshold = (value: number, mode: ThresholdMode): number => + mode === "percent" ? Math.min(100, Math.max(0, Math.trunc(value))) : value; + +export const patchmonOptionsSuperRefine = (data: Record, ctx: RefinementCtx) => { + if (!data.enableThresholdColors || !data.useCustomThresholds) { + return; + } + + for (const { mode, warningAt, criticalAt } of PATCHMON_THRESHOLD_PAIRS) { + if (data[mode] !== "percent") { + continue; + } + + for (const key of [warningAt, criticalAt] as const) { + const result = percentThresholdSchema.safeParse(data[key]); + + if (!result.success) { + for (const issue of result.error.issues) { + ctx.addIssue({ ...issue, path: [key] }); + } + } + } + } +}; diff --git a/packages/widgets/src/releases/component.tsx b/packages/widgets/src/releases/component.tsx index cc41e8b68..f0e32df7d 100644 --- a/packages/widgets/src/releases/component.tsx +++ b/packages/widgets/src/releases/component.tsx @@ -20,8 +20,9 @@ import ReactMarkdown from "react-markdown"; import { clientApi } from "@homarr/api/client"; import { useRequiredBoard } from "@homarr/boards/context"; -import { isDateWithin, isNullOrWhitespace, splitToChunksWithNItems } from "@homarr/common"; -import { getIconUrl } from "@homarr/definitions"; +import { createId, isDateWithin, isNullOrWhitespace, splitToChunksWithNItems } from "@homarr/common"; +import type { ReleaseProviderKind } from "@homarr/definitions"; +import { getReleaseProviderIconUrl, getReleaseProviderName } from "@homarr/definitions"; import { useScopedI18n } from "@homarr/translation/client"; import { MaskedOrNormalImage } from "@homarr/ui"; @@ -35,7 +36,7 @@ const formatRelativeDate = (value: string): string => { return isMonths ? value.toUpperCase() : isOtherUnits ? value.toLowerCase() : value; }; -export default function ReleasesWidget({ options }: WidgetComponentProps<"releases">) { +export default function ReleasesWidget({ options, itemId }: WidgetComponentProps<"releases">) { const t = useScopedI18n("widget.releases"); const now = useNow(); const formatter = useFormatter(); @@ -55,11 +56,16 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas [options.newReleaseWithin, options.staleReleaseWithin], ); - // Group repositories by integration + const normalizedRepositories = useMemo( + () => options.repositories.map((repo) => (repo.id ? repo : { ...repo, id: createId() })), + [options.repositories], + ); + + // Group repositories by provider const groupedRepositories = useMemo(() => { - return options.repositories.reduce( + return normalizedRepositories.reduce( (acc, repo) => { - const key = repo.providerIntegrationId; + const key = repo.provider; if (!key) return acc; acc[key] ??= []; @@ -67,28 +73,30 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas return acc; }, - {} as Record, + {} as Partial>, ); - }, [options.repositories]); + }, [normalizedRepositories]); // For each group, split into chunks of 5 const batchedRepositories = useMemo(() => { - return Object.entries(groupedRepositories).flatMap(([integrationId, group]) => + return Object.entries(groupedRepositories).flatMap(([provider, group]) => splitToChunksWithNItems(group, 5).map((chunk) => ({ - integrationId, + provider: provider as ReleaseProviderKind, repositories: chunk, })), ); }, [groupedRepositories]); - const queryResults = clientApi.useQueries((t) => - batchedRepositories.flatMap(({ integrationId, repositories }) => - t.widget.releases.getLatest({ - integrationId, + const queryResults = clientApi.useQueries((trpc) => + batchedRepositories.flatMap(({ provider, repositories }) => + trpc.widget.releases.getLatest({ + itemId, repositories: repositories.map((repository) => ({ id: repository.id, + provider, identifier: repository.identifier, versionFilter: repository.versionFilter, + providerUrl: repository.providerUrl, })), }), ), @@ -100,15 +108,15 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas ); const repositories = useMemo(() => { - const formattedResults = options.repositories + const formattedResults = normalizedRepositories .map((repository) => { - if (!repository.providerIntegrationId) return { ...repository, error: { code: "noProviderSeleceted" } }; + if (!repository.provider) return { ...repository, error: { code: "noProviderSelected" } }; const repositoryResult = results.flat().find(({ id }) => id === repository.id); if (!repositoryResult) return { ...repository, error: { code: "noProviderResponse" } }; if (!repositoryResult.success) return { ...repository, error: repositoryResult.error }; - const { data: release, integration } = repositoryResult; + const { data: release, provider } = repositoryResult; const isReleaseWithin = (relativeDate: string) => Boolean(relativeDate) && isDateWithin(release.latestReleaseAt, relativeDate); @@ -116,7 +124,7 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas return { ...repository, ...release, - integration: { name: integration.name, iconUrl: getIconUrl(integration.kind) }, + providerMetadata: { name: getReleaseProviderName(provider), iconUrl: getReleaseProviderIconUrl(provider) }, isNewRelease: isReleaseWithin(relativeDateOptions.newReleaseWithin), isStaleRelease: !isReleaseWithin(relativeDateOptions.staleReleaseWithin), viewed: releasesViewedList[repository.id] === release.latestRelease, @@ -139,7 +147,7 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas return formattedResults; }, [ results, - options.repositories, + normalizedRepositories, options.showOnlyHighlighted, options.topReleases, relativeDateOptions.newReleaseWithin, @@ -173,7 +181,7 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas className={combineClasses( "releases-repository", // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - `releases-repository-${repository.integration?.name ?? "error"}-${repository.name || repository.identifier.replace(/[^a-zA-Z0-9]/g, "_")}`, + `releases-repository-${repository.providerMetadata?.name ?? "error"}-${repository.name || repository.identifier.replace(/[^a-zA-Z0-9]/g, "_")}`, classes.releasesRepository, )} gap={0} @@ -187,7 +195,7 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas > {hasError ? ( @@ -529,28 +537,23 @@ const ExpandedDisplay = ({ p={10} > - + {repository.identifier} - {repository.integration && ( + {repository.providerMetadata && ( - - {repository.integration.name} + + {repository.providerMetadata.name} )} @@ -564,7 +567,7 @@ const ExpandedDisplay = ({ {formatter.relativeTime(repository.createdAt, { now, - style: "narrow", + style: "long", })} @@ -614,7 +617,7 @@ const ExpandedDisplay = ({ {repository.error && ( <> - + <Title className="releases-repository-expanded-error-title" order={4} ta="center" c="red"> {t("error.label")} - {repository.error.code ? t(`error.messages.${repository.error.code}` as never) : repository.error.message} + {repository.error.message ?? + (repository.error.code ? t(`error.messages.${repository.error.code}` as never) : null)} )} diff --git a/packages/widgets/src/releases/index.ts b/packages/widgets/src/releases/index.ts index 8de5227ad..2c3d1d3a2 100644 --- a/packages/widgets/src/releases/index.ts +++ b/packages/widgets/src/releases/index.ts @@ -1,6 +1,9 @@ import { IconRocket } from "@tabler/icons-react"; import { z } from "zod/v4"; +import { createId } from "@homarr/common"; +import { releaseProviderKinds } from "@homarr/definitions"; + import { createWidgetDefinition } from "../definition"; import { optionsBuilder } from "../options"; @@ -39,10 +42,20 @@ export const { definition, componentLoader } = createWidgetDefinition("releases" validate: z.number().min(0), }), repositories: factory.multiReleasesRepositories({ - defaultValue: [], + defaultValue: [ + { + id: createId(), + provider: "github" as const, + identifier: "homarr-labs/homarr", + name: "Homarr", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/homarr.png", + versionFilter: { prefix: "v", precision: 3 }, + }, + ], validate: z.array( z.object({ - providerIntegrationId: z.string().optional(), + id: z.string().default(() => createId()), + provider: z.enum(releaseProviderKinds).optional(), identifier: z.string().min(1), name: z.string().optional(), versionFilter: z @@ -53,6 +66,7 @@ export const { definition, componentLoader } = createWidgetDefinition("releases" }) .optional(), iconUrl: z.string().optional(), + providerUrl: z.string().url().optional(), }), ), }), diff --git a/packages/widgets/src/releases/releases-repository.ts b/packages/widgets/src/releases/releases-repository.ts index 157d9a146..3da40932a 100644 --- a/packages/widgets/src/releases/releases-repository.ts +++ b/packages/widgets/src/releases/releases-repository.ts @@ -1,3 +1,5 @@ +import type { ReleaseProviderKind } from "@homarr/definitions"; + export interface ReleasesVersionFilter { prefix?: string; precision: number; @@ -6,11 +8,12 @@ export interface ReleasesVersionFilter { export interface ReleasesRepository { id: string; - providerIntegrationId?: string; + provider?: ReleaseProviderKind; identifier: string; name?: string; versionFilter?: ReleasesVersionFilter; iconUrl?: string; + providerUrl?: string; } export interface ReleasesRepositoryResponse extends ReleasesRepository { @@ -32,7 +35,7 @@ export interface ReleasesRepositoryResponse extends ReleasesRepository { forksCount?: number; openIssues?: number; - integration?: { + providerMetadata?: { name: string; iconUrl?: string; }; diff --git a/packages/widgets/src/rssFeed/component.tsx b/packages/widgets/src/rssFeed/component.tsx index a2cf07ebb..626f87328 100644 --- a/packages/widgets/src/rssFeed/component.tsx +++ b/packages/widgets/src/rssFeed/component.tsx @@ -12,29 +12,7 @@ import type { WidgetComponentProps } from "../definition"; import classes from "./component.module.scss"; const useLiveFeedEntries = (input: RouterInputs["widget"]["rssFeed"]["getFeeds"]) => { - const { data: feedEntries = [] } = clientApi.widget.rssFeed.getFeeds.useQuery(input, { - staleTime: 5 * 60 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }); - const utils = clientApi.useUtils(); - - clientApi.widget.rssFeed.subscribeFeeds.useSubscription(input, { - onData(updatedData) { - utils.widget.rssFeed.getFeeds.setData(input, (oldData) => { - return oldData - ?.filter((entry) => entry.feedUrl !== updatedData.url) - .concat(updatedData.entries) - .toSorted((entryA, entryB) => { - return entryA.published && entryB.published - ? new Date(entryB.published).getTime() - new Date(entryA.published).getTime() - : 0; - }) - .slice(0, input.maximumAmountPosts); - }); - }, - }); + const { data: feedEntries = [] } = clientApi.widget.rssFeed.getFeeds.useQuery(input); return feedEntries; }; diff --git a/packages/widgets/src/smart-home/entity-state/component.tsx b/packages/widgets/src/smart-home/entity-state/component.tsx index 2ad45b7a3..f1d6b6e18 100644 --- a/packages/widgets/src/smart-home/entity-state/component.tsx +++ b/packages/widgets/src/smart-home/entity-state/component.tsx @@ -23,20 +23,13 @@ export default function SmartHomeEntityStateWidget({ entityId: options.entityId, integrationId, }; - const { data: entityState } = clientApi.widget.smartHome.entityState.useQuery(input, { - staleTime: 60 * 1000, - }); + const { data: entityState } = clientApi.widget.smartHome.entityState.useQuery(input); const utils = clientApi.useUtils(); - - clientApi.widget.smartHome.subscribeEntityState.useSubscription(input, { - onData(data) { - utils.widget.smartHome.entityState.setData(input, data.state); - }, + const { mutate } = clientApi.widget.smartHome.switchEntity.useMutation({ + onSettled: () => void utils.widget.smartHome.entityState.invalidate(input), }); - const { mutate } = clientApi.widget.smartHome.switchEntity.useMutation(); - const attribute = options.entityUnit.length > 0 ? " " + options.entityUnit : ""; const handleClick = useCallback(() => { diff --git a/packages/widgets/src/speedtest-tracker/component.tsx b/packages/widgets/src/speedtest-tracker/component.tsx index dcde145a4..4b2b83dbb 100644 --- a/packages/widgets/src/speedtest-tracker/component.tsx +++ b/packages/widgets/src/speedtest-tracker/component.tsx @@ -14,34 +14,9 @@ import { mergeStats } from "./helpers"; import { LatestResultSection } from "./latest-result"; import { RecentResultsSection } from "./recent-results"; -export default function SpeedtestTrackerWidget({ - options, - integrationIds, - isEditMode, -}: WidgetComponentProps<"speedtestTracker">) { +export default function SpeedtestTrackerWidget({ options, integrationIds }: WidgetComponentProps<"speedtestTracker">) { const t = useScopedI18n("widget.speedtestTracker"); - const { data: dashboardData = [] } = clientApi.widget.speedtestTracker.getDashboard.useQuery( - { integrationIds }, - { staleTime: 10 * 60 * 1000 }, - ); - - const utils = clientApi.useUtils(); - clientApi.widget.speedtestTracker.subscribeToDashboard.useSubscription( - { integrationIds }, - { - enabled: !isEditMode, - onData(newData) { - utils.widget.speedtestTracker.getDashboard.setData({ integrationIds }, (prevData) => { - if (!prevData) return prevData; - return prevData.map((instance) => - instance.integrationId === newData.integrationId - ? { ...instance, dashboard: newData.dashboard, updatedAt: newData.timestamp } - : instance, - ); - }); - }, - }, - ); + const { data: dashboardData = [] } = clientApi.widget.speedtestTracker.getDashboard.useQuery({ integrationIds }); const combined = useMemo( () => diff --git a/packages/widgets/src/stocks/component.tsx b/packages/widgets/src/stocks/component.tsx index 99078b4d2..918851ce8 100644 --- a/packages/widgets/src/stocks/component.tsx +++ b/packages/widgets/src/stocks/component.tsx @@ -25,9 +25,7 @@ function calculateChangePercentage(currentPrice: number, previousClose: number) export default function StockPriceWidget({ options, width, height }: WidgetComponentProps<"stockPrice">) { const t = useScopedI18n("widget.stockPrice"); const theme = useMantineTheme(); - const { data: result } = clientApi.widget.stockPrice.getPriceHistory.useQuery(options, { - staleTime: 5 * 60 * 1000, - }); + const { data: result } = clientApi.widget.stockPrice.getPriceHistory.useQuery(options); if (!result) return ; const { data } = result; diff --git a/packages/widgets/src/storage-volume-options.ts b/packages/widgets/src/storage-volume-options.ts new file mode 100644 index 000000000..202da2c8a --- /dev/null +++ b/packages/widgets/src/storage-volume-options.ts @@ -0,0 +1,39 @@ +import { clientApi } from "@homarr/api/client"; + +import { toScopedStorageVolumeValue } from "./filter-storage-volumes"; + +export const createStorageVolumeMultiSelectOptions = () => ({ + defaultValue: [] as string[], + withDescription: true, + useOptions: (integrationIds: string[]) => { + const queryResults = clientApi.useQueries((queryBuilder) => + integrationIds.map((integrationId) => + queryBuilder.widget.healthMonitoring.listStorageVolumes({ + integrationId, + }), + ), + ); + + const isPending = queryResults.some((queryResult) => queryResult.isPending); + const isError = queryResults.every((queryResult) => queryResult.isError); + const options = queryResults.flatMap((queryResult, index) => { + const integrationId = integrationIds[index]; + if (!integrationId) { + return []; + } + + return (queryResult.data ?? []).map((option) => ({ + label: option.label, + value: toScopedStorageVolumeValue(integrationId, option.value), + })); + }); + + const uniqueOptions = [...new Map(options.map((option) => [option.value, option])).values()]; + + return { + data: uniqueOptions, + isPending, + isError: isError && uniqueOptions.length === 0, + }; + }, +}); diff --git a/packages/widgets/src/system-disks/component.tsx b/packages/widgets/src/system-disks/component.tsx index efe2fe73d..cabc03ba5 100644 --- a/packages/widgets/src/system-disks/component.tsx +++ b/packages/widgets/src/system-disks/component.tsx @@ -10,6 +10,7 @@ import { useI18n } from "@homarr/translation/client"; import { WidgetEmptyState } from "../common/empty-state"; import type { WidgetComponentProps } from "../definition"; +import { filterStorageVolumes } from "../filter-storage-volumes"; import { NoIntegrationDataError } from "../errors/no-data-integration"; type DisplayMode = WidgetComponentProps<"systemDisks">["options"]["displayMode"]; @@ -118,28 +119,30 @@ const SystemDiskCard = ({ export default function SystemResources({ integrationIds, options }: WidgetComponentProps<"systemDisks">) { const queryInput = { integrationIds }; - const { data = [] } = clientApi.widget.healthMonitoring.getSystemHealthStatus.useQuery(queryInput, { - staleTime: 5 * 1000, - }); - const utils = clientApi.useUtils(); - - clientApi.widget.healthMonitoring.subscribeSystemHealthStatus.useSubscription(queryInput, { - onData(data) { - utils.widget.healthMonitoring.getSystemHealthStatus.setData(queryInput, (oldData) => - oldData?.map((item) => (item.integrationId === data.integrationId ? { ...item, ...data } : item)), - ); - }, - }); + const { data = [] } = clientApi.widget.healthMonitoring.getSystemHealthStatus.useQuery(queryInput); const lastItem = data.at(-1); if (!lastItem) return ; - const { fileSystem, smart } = lastItem.healthInfo; - if (fileSystem.length === 0) { + const rawFileSystem = lastItem.healthInfo.fileSystem; + if (rawFileSystem.length === 0) { throw new NoIntegrationDataError(); } + const fileSystem = filterStorageVolumes( + rawFileSystem, + options.visibleStorageVolumes, + lastItem.integrationId, + ); + const smart = filterStorageVolumes( + lastItem.healthInfo.smart, + options.visibleStorageVolumes, + lastItem.integrationId, + ); + + if (fileSystem.length === 0) return ; + return ( {fileSystem.map((item) => { diff --git a/packages/widgets/src/system-disks/index.ts b/packages/widgets/src/system-disks/index.ts index 99ea9a70a..61e4a5c05 100644 --- a/packages/widgets/src/system-disks/index.ts +++ b/packages/widgets/src/system-disks/index.ts @@ -2,21 +2,32 @@ import { IconServer2 } from "@tabler/icons-react"; import { createWidgetDefinition } from "../definition"; import { optionsBuilder } from "../options"; +import { createStorageVolumeMultiSelectOptions } from "../storage-volume-options"; export const { definition, componentLoader } = createWidgetDefinition("systemDisks", { icon: IconServer2, - supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid"], + supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid", "synology"], createOptions() { - return optionsBuilder.from((factory) => ({ - showTemperatureIfAvailable: factory.switch({ defaultValue: true }), - displayMode: factory.select({ - options: (["percentage", "absolute", "free"] as const).map((value) => ({ - value, - label: (t) => t(`widget.systemDisks.option.displayMode.option.${value}.label`), - })), - defaultValue: "percentage", + return optionsBuilder.from( + (factory) => ({ + showTemperatureIfAvailable: factory.switch({ defaultValue: true }), + displayMode: factory.select({ + options: (["percentage", "absolute", "free"] as const).map((value) => ({ + value, + label: (t) => t(`widget.systemDisks.option.displayMode.option.${value}.label`), + })), + defaultValue: "percentage", + }), + showBackgroundBar: factory.switch({ defaultValue: true }), + visibleStorageVolumes: factory.integrationMultiSelect(createStorageVolumeMultiSelectOptions()), }), - showBackgroundBar: factory.switch({ defaultValue: true }), - })); + { + visibleStorageVolumes: { + shouldHide(_, integrationKinds) { + return integrationKinds.length === 0 || !integrationKinds.every((kind) => kind === "synology"); + }, + }, + }, + ); }, }).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/system-resources/component.tsx b/packages/widgets/src/system-resources/component.tsx index d9e5a8604..1d3854c89 100644 --- a/packages/widgets/src/system-resources/component.tsx +++ b/packages/widgets/src/system-resources/component.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Box, Group, Stack } from "@mantine/core"; import { useElementSize } from "@mantine/hooks"; @@ -15,56 +15,49 @@ import { NetworkTrafficChart } from "./chart/network-traffic"; const MAX_QUEUE_SIZE = 15; +const toChartItem = (healthInfo: { + cpuUtilization: number; + memUsedInBytes: number; + gpu: { processorUtilization: number }[]; + network: { up: number; down: number } | null; +}) => ({ + cpu: healthInfo.cpuUtilization, + memory: healthInfo.memUsedInBytes, + gpu: + healthInfo.gpu.length > 0 + ? healthInfo.gpu.reduce((acc, g) => acc + g.processorUtilization, 0) / healthInfo.gpu.length + : 0, + network: healthInfo.network, +}); + export default function SystemResources({ integrationIds, options }: WidgetComponentProps<"systemResources">) { const { ref, width } = useElementSize(); - const { data = [] } = clientApi.widget.healthMonitoring.getSystemHealthStatus.useQuery( - { - integrationIds, - }, - { - staleTime: 5 * 1000, - }, - ); + const { data = [], dataUpdatedAt } = clientApi.widget.healthMonitoring.getSystemHealthStatus.useQuery({ + integrationIds, + }); const memoryCapacityInBytes = (data[0]?.healthInfo.memAvailableInBytes ?? 0) + (data[0]?.healthInfo.memUsedInBytes ?? 0); const [items, setItems] = useState< { cpu: number; memory: number; gpu: number; network: { up: number; down: number } | null }[] - >( - data.map((item) => ({ - cpu: item.healthInfo.cpuUtilization, - memory: item.healthInfo.memUsedInBytes, - gpu: - item.healthInfo.gpu.length > 0 - ? item.healthInfo.gpu.reduce((acc, g) => acc + g.processorUtilization, 0) / item.healthInfo.gpu.length - : 0, - network: item.healthInfo.network, - })), - ); + >(() => (data[0] ? [toChartItem(data[0].healthInfo)] : [])); - clientApi.widget.healthMonitoring.subscribeSystemHealthStatus.useSubscription( - { - integrationIds, - }, - { - onData(data) { - setItems((previousItems) => { - const next = { - cpu: data.healthInfo.cpuUtilization, - memory: data.healthInfo.memUsedInBytes, - gpu: - data.healthInfo.gpu.length > 0 - ? data.healthInfo.gpu.reduce((acc, g) => acc + g.processorUtilization, 0) / data.healthInfo.gpu.length - : 0, - network: data.healthInfo.network, - }; + const prevIntegrationIds = useRef(integrationIds); + useEffect(() => { + if (prevIntegrationIds.current === integrationIds) return; + prevIntegrationIds.current = integrationIds; + setItems([]); + }, [integrationIds]); - return [...previousItems, next].slice(-MAX_QUEUE_SIZE); - }); - }, - }, - ); + const lastUpdatedAt = useRef(dataUpdatedAt); + useEffect(() => { + if (dataUpdatedAt === lastUpdatedAt.current) return; + lastUpdatedAt.current = dataUpdatedAt; + const firstItem = data[0]; + if (!firstItem) return; + setItems((prev) => [...prev, toChartItem(firstItem.healthInfo)].slice(-MAX_QUEUE_SIZE)); + }, [dataUpdatedAt, data]); const showNetwork = items.length === 0 || (items.every((item) => item.network !== null) && options.visibleCharts.includes("network")); diff --git a/packages/widgets/src/system-resources/index.ts b/packages/widgets/src/system-resources/index.ts index 68fffd4ed..8d4837187 100644 --- a/packages/widgets/src/system-resources/index.ts +++ b/packages/widgets/src/system-resources/index.ts @@ -14,7 +14,7 @@ const labelDisplayModeOptions = { export const { definition, componentLoader } = createWidgetDefinition("systemResources", { icon: IconGraphFilled, - supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid", "glances"], + supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid", "glances", "synology"], createOptions() { return optionsBuilder.from((factory) => ({ hasShadow: factory.switch({ defaultValue: true }), diff --git a/packages/widgets/src/timetable/component.tsx b/packages/widgets/src/timetable/component.tsx index 539d7e6c7..47751a751 100644 --- a/packages/widgets/src/timetable/component.tsx +++ b/packages/widgets/src/timetable/component.tsx @@ -7,35 +7,27 @@ import { useScopedI18n } from "@homarr/translation/client"; import type { DynamicSelectOption } from "../_inputs/widget-dynamic-select-input"; import type { WidgetComponentProps } from "../definition"; -export default function TimetableWidget({ options, integrationIds }: WidgetComponentProps<"timetable">) { - // It will always have at least one integration as otherwise the NoIntegrationSelectedError would be thrown in item-content.tsx - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const integrationId = integrationIds[0]!; +export default function TimetableWidget({ options }: WidgetComponentProps<"timetable">) { const t = useScopedI18n("widget.timetable"); if (!options.station) { return
{t("noStation")}
; } - return ; + return ; } interface TimetableWidgetInnerProps { station: DynamicSelectOption; - integrationId: string; + baseUrl: string; } -const TimetableWidgetInner = ({ station, integrationId }: TimetableWidgetInnerProps) => { - const { data: timetable } = clientApi.widget.timetable.getTimetable.useQuery( - { - integrationId, - stationId: station.value, - limit: 10, - }, - { - staleTime: 60 * 1000, - }, - ); +const TimetableWidgetInner = ({ station, baseUrl }: TimetableWidgetInnerProps) => { + const { data: timetable } = clientApi.widget.timetable.getTimetable.useQuery({ + baseUrl, + stationId: station.value, + limit: 10, + }); const t = useScopedI18n("widget.timetable"); return ( diff --git a/packages/widgets/src/timetable/index.ts b/packages/widgets/src/timetable/index.ts index f303b8c62..cee44323c 100644 --- a/packages/widgets/src/timetable/index.ts +++ b/packages/widgets/src/timetable/index.ts @@ -1,4 +1,5 @@ import { IconBusStop } from "@tabler/icons-react"; +import { z } from "zod/v4"; import { clientApi } from "@homarr/api/client"; @@ -7,20 +8,18 @@ import { optionsBuilder } from "../options"; export const { componentLoader, definition } = createWidgetDefinition("timetable", { icon: IconBusStop, - supportedIntegrations: ["searchCh"], createOptions() { return optionsBuilder.from((factory) => ({ + baseUrl: factory.text({ + defaultValue: "https://search.ch", + validate: z.string().url(), + }), station: factory.dynamicSelect({ - useOptions(query, integrationIds) { - const { data: stations, isPending } = clientApi.widget.timetable.searchStations.useQuery( - { - integrationId: integrationIds[0] ?? "", - query, - }, - { - enabled: integrationIds.length > 0, - }, - ); + useOptions(query, _integrationIds, options) { + const { data: stations, isPending } = clientApi.widget.timetable.searchStations.useQuery({ + baseUrl: typeof options.baseUrl === "string" ? options.baseUrl : "https://search.ch", + query, + }); return { isPending, diff --git a/packages/widgets/src/tracearr/component.tsx b/packages/widgets/src/tracearr/component.tsx index 0ff5e9132..f3b70192a 100644 --- a/packages/widgets/src/tracearr/component.tsx +++ b/packages/widgets/src/tracearr/component.tsx @@ -13,50 +13,23 @@ import { StatsBar } from "./stats-section"; import { StreamsList } from "./streams-section"; import { ViolationsList } from "./violations-section"; -export default function TracearrWidget({ - options, - integrationIds, - width, - isEditMode, -}: WidgetComponentProps<"tracearr">) { +export default function TracearrWidget({ options, integrationIds, width }: WidgetComponentProps<"tracearr">) { if (integrationIds.length === 0) { throw new NoIntegrationDataError(); } - return ; + return ; } interface TracearrContentProps { integrationIds: string[]; options: WidgetComponentProps<"tracearr">["options"]; width: number; - isEditMode: boolean; } -function TracearrContent({ integrationIds, options, width, isEditMode }: TracearrContentProps) { +function TracearrContent({ integrationIds, options, width }: TracearrContentProps) { const t = useScopedI18n("widget.tracearr"); - const { data: dashboardData = [] } = clientApi.widget.tracearr.getDashboard.useQuery( - { integrationIds }, - { staleTime: 5 * 1000 }, - ); - - const utils = clientApi.useUtils(); - clientApi.widget.tracearr.subscribeToDashboard.useSubscription( - { integrationIds }, - { - enabled: !isEditMode, - onData(newData) { - utils.widget.tracearr.getDashboard.setData({ integrationIds }, (prevData) => { - if (!prevData) return prevData; - return prevData.map((instance) => - instance.integrationId === newData.integrationId - ? { ...instance, dashboard: newData.dashboard, updatedAt: newData.timestamp } - : instance, - ); - }); - }, - }, - ); + const { data: dashboardData = [] } = clientApi.widget.tracearr.getDashboard.useQuery({ integrationIds }); // Merge data from all integrations const combined = dashboardData.reduce( diff --git a/packages/widgets/src/traefik/component.module.css b/packages/widgets/src/traefik/component.module.css new file mode 100644 index 000000000..59bede44b --- /dev/null +++ b/packages/widgets/src/traefik/component.module.css @@ -0,0 +1,125 @@ +.root { + container-type: inline-size; + display: flex; + flex-direction: column; + min-height: 100%; + padding: clamp(6px, 3cqi, 12px); + gap: clamp(6px, 3cqi, 12px); +} + +.titleBlock { + min-width: 0; +} + +.title { + font-size: clamp(12px, 4cqi, 15px); + font-weight: 700; + line-height: 1.2; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.subtitle { + font-size: clamp(9px, 3cqi, 12px); + color: var(--mantine-color-dimmed); + line-height: 1.2; +} + +.hero { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: clamp(6px, 3cqi, 10px); +} + +.metric { + display: flex; + align-items: center; + min-width: 0; + gap: clamp(5px, 2cqi, 8px); + padding: clamp(6px, 3cqi, 10px); + border-radius: var(--mantine-radius-sm); + background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6)); +} + +.metricIcon { + flex-shrink: 0; + color: var(--mantine-color-teal-6); +} + +.metricText { + min-width: 0; +} + +.metricValue { + font-size: clamp(16px, 7cqi, 24px); + font-weight: 800; + line-height: 1; +} + +.metricLabel { + font-size: clamp(8px, 2.8cqi, 11px); + color: var(--mantine-color-dimmed); + line-height: 1.2; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.protocolGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); + gap: clamp(6px, 3cqi, 10px); +} + +.protocolCard { + min-width: 0; + padding: clamp(6px, 3cqi, 10px); + border: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4)); + border-radius: var(--mantine-radius-sm); +} + +.protocolTitle { + font-size: clamp(10px, 3.5cqi, 13px); + font-weight: 700; + line-height: 1.2; + text-transform: uppercase; +} + +.protocolRows { + display: flex; + flex-direction: column; + gap: 4px; + margin-top: 6px; +} + +.protocolRow { + display: flex; + align-items: center; + justify-content: space-between; + min-width: 0; + gap: 6px; +} + +.protocolLabel { + min-width: 0; + color: var(--mantine-color-dimmed); + font-size: clamp(9px, 3cqi, 12px); +} + +.protocolLabel p { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.counts { + flex-shrink: 0; + font-size: clamp(10px, 3.5cqi, 13px); +} + +.entryPoints { + display: flex; + flex-wrap: wrap; + gap: 4px; +} diff --git a/packages/widgets/src/traefik/component.tsx b/packages/widgets/src/traefik/component.tsx new file mode 100644 index 000000000..69ec3e811 --- /dev/null +++ b/packages/widgets/src/traefik/component.tsx @@ -0,0 +1,259 @@ +"use client"; + +import type { ReactNode } from "react"; +import { Badge, Group, ScrollArea, Text } from "@mantine/core"; +import { + IconAlertTriangle, + IconCircleCheck, + IconDoorEnter, + IconRoute, + IconServer, + IconStack2, +} from "@tabler/icons-react"; + +import { clientApi } from "@homarr/api/client"; +import type { TraefikDashboardData, TraefikProtocolSummary, TraefikResourceSummary } from "@homarr/integrations/types"; +import { useScopedI18n } from "@homarr/translation/client"; + +import { WidgetEmptyState } from "../common/empty-state"; +import type { WidgetComponentProps } from "../definition"; +import { NoIntegrationDataError } from "../errors/no-data-integration"; +import classes from "./component.module.css"; + +const emptySummary: TraefikResourceSummary = { + total: 0, + enabled: 0, + warnings: 0, + errors: 0, +}; + +const emptyProtocol: TraefikProtocolSummary = { + routers: emptySummary, + services: emptySummary, + middlewares: emptySummary, +}; + +const emptyDashboard: TraefikDashboardData = { + version: null, + entryPoints: [], + http: emptyProtocol, + tcp: emptyProtocol, + udp: { + routers: emptySummary, + services: emptySummary, + }, +}; + +type ProtocolKey = "http" | "tcp" | "udp"; +type ResourceKey = "routers" | "services" | "middlewares"; + +export default function TraefikWidget({ integrationIds, options, width }: WidgetComponentProps<"traefik">) { + if (integrationIds.length === 0) { + throw new NoIntegrationDataError(); + } + + const t = useScopedI18n("widget.traefik"); + const { data } = clientApi.widget.traefik.getDashboard.useQuery({ integrationIds }); + + if (!data) return ; + + const combined = data.reduce( + (acc, item) => ({ + version: acc.version ?? item.dashboard.version, + entryPoints: [...acc.entryPoints, ...item.dashboard.entryPoints], + http: combineProtocol(acc.http, item.dashboard.http), + tcp: combineProtocol(acc.tcp, item.dashboard.tcp), + udp: { + routers: combineSummary(acc.udp.routers, item.dashboard.udp.routers), + services: combineSummary(acc.udp.services, item.dashboard.udp.services), + }, + }), + emptyDashboard, + ); + + const totalRouters = combined.http.routers.total + combined.tcp.routers.total + combined.udp.routers.total; + const totalErrors = getProtocolErrors(combined.http) + getProtocolErrors(combined.tcp) + getUdpErrors(combined.udp); + const totalWarnings = + getProtocolWarnings(combined.http) + getProtocolWarnings(combined.tcp) + getUdpWarnings(combined.udp); + const protocolKeys: ProtocolKey[] = ["http"]; + if (options.showTcp) protocolKeys.push("tcp"); + if (options.showUdp) protocolKeys.push("udp"); + + return ( + +
+ +
+ {data.length === 1 ? data[0]?.integrationName : t("instances")} + {combined.version ? `v${combined.version}` : t("versionUnknown")} +
+ +
+ +
+ } + label={t("summary.routers")} + value={totalRouters} + /> + } + label={t("summary.entryPoints")} + value={dedupe(combined.entryPoints).length} + /> +
+ +
+ {protocolKeys.map((protocol) => ( + + ))} +
+ + {options.showEntryPoints && combined.entryPoints.length > 0 && ( +
+ {dedupe(combined.entryPoints) + .slice(0, getEntryPointLimit(width)) + .map((entryPoint) => ( + + {entryPoint} + + ))} +
+ )} +
+
+ ); +} + +function ProtocolCard({ protocol, data }: { protocol: ProtocolKey; data: TraefikDashboardData[ProtocolKey] }) { + const t = useScopedI18n("widget.traefik"); + const rows: { key: ResourceKey; icon: typeof IconRoute; summary: TraefikResourceSummary }[] = [ + { key: "routers", icon: IconRoute, summary: data.routers }, + { key: "services", icon: IconServer, summary: data.services }, + ]; + if ("middlewares" in data) { + rows.push({ key: "middlewares", icon: IconStack2, summary: data.middlewares }); + } + + return ( +
+ {protocol.toUpperCase()} +
+ {rows.map(({ key, icon: Icon, summary }) => ( +
+ + + {t(`resource.${key}`)} + + +
+ ))} +
+
+ ); +} + +function StatusCount({ summary }: { summary: TraefikResourceSummary }) { + return ( + + {summary.total} + {summary.errors > 0 && ( + + {summary.errors} + + )} + {summary.warnings > 0 && ( + + {summary.warnings} + + )} + + ); +} + +function SummaryMetric({ icon, label, value }: { icon: ReactNode; label: string; value: number }) { + return ( +
+
{icon}
+
+ {value} + {label} +
+
+ ); +} + +function HealthBadge({ errors, warnings }: { errors: number; warnings: number }) { + const t = useScopedI18n("widget.traefik"); + + if (errors > 0) { + return ( + } radius="sm"> + {t("status.errors", { count: errors })} + + ); + } + + if (warnings > 0) { + return ( + } radius="sm"> + {t("status.warnings", { count: warnings })} + + ); + } + + return ( + } radius="sm"> + {t("status.healthy")} + + ); +} + +function combineProtocol(left: TraefikProtocolSummary, right: TraefikProtocolSummary): TraefikProtocolSummary { + return { + routers: combineSummary(left.routers, right.routers), + services: combineSummary(left.services, right.services), + middlewares: combineSummary(left.middlewares, right.middlewares), + }; +} + +function combineSummary(left: TraefikResourceSummary, right: TraefikResourceSummary): TraefikResourceSummary { + return { + total: left.total + right.total, + enabled: left.enabled + right.enabled, + warnings: left.warnings + right.warnings, + errors: left.errors + right.errors, + }; +} + +function getProtocolErrors(protocol: TraefikProtocolSummary) { + return protocol.routers.errors + protocol.services.errors + protocol.middlewares.errors; +} + +function getProtocolWarnings(protocol: TraefikProtocolSummary) { + return protocol.routers.warnings + protocol.services.warnings + protocol.middlewares.warnings; +} + +function getUdpErrors(protocol: TraefikDashboardData["udp"]) { + return protocol.routers.errors + protocol.services.errors; +} + +function getUdpWarnings(protocol: TraefikDashboardData["udp"]) { + return protocol.routers.warnings + protocol.services.warnings; +} + +function dedupe(values: string[]) { + return Array.from(new Set(values)); +} + +function getHeroIconSize(width: number) { + if (width < 220) return 16; + if (width < 340) return 18; + return 22; +} + +function getEntryPointLimit(width: number) { + if (width < 220) return 2; + if (width < 360) return 4; + return 8; +} diff --git a/packages/widgets/src/traefik/index.ts b/packages/widgets/src/traefik/index.ts new file mode 100644 index 000000000..854075121 --- /dev/null +++ b/packages/widgets/src/traefik/index.ts @@ -0,0 +1,28 @@ +import { IconRoute, IconRouteOff } from "@tabler/icons-react"; + +import { createWidgetDefinition } from "../definition"; +import { optionsBuilder } from "../options"; + +export const { definition, componentLoader } = createWidgetDefinition("traefik", { + icon: IconRoute, + createOptions() { + return optionsBuilder.from((factory) => ({ + showTcp: factory.switch({ + defaultValue: true, + }), + showUdp: factory.switch({ + defaultValue: true, + }), + showEntryPoints: factory.switch({ + defaultValue: true, + }), + })); + }, + supportedIntegrations: ["traefik"], + errors: { + INTERNAL_SERVER_ERROR: { + icon: IconRouteOff, + message: (t) => t("widget.traefik.error.internalServerError"), + }, + }, +}).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/umami/umami-content.tsx b/packages/widgets/src/umami/umami-content.tsx index 2f8dea22c..2a187c3d5 100644 --- a/packages/widgets/src/umami/umami-content.tsx +++ b/packages/widgets/src/umami/umami-content.tsx @@ -10,7 +10,7 @@ import { useScopedI18n } from "@homarr/translation/client"; import { UmamiEventsContent } from "./umami-events-content"; import { UmamiTopPagesContent, UmamiTopReferrersContent } from "./umami-top-list"; -import { formatTimeFrameLabel, formatXLabel } from "./umami-utils"; +import { formatTimeFrameLabel, formatXLabel, umamiQueryOptions } from "./umami-utils"; import type { TimeFrame } from "./umami-utils"; interface UmamiContentProps { @@ -41,29 +41,19 @@ export function UmamiContent({ const tickColor = colorScheme === "dark" ? "#c1c2c5" : "#495057"; const { data: results = [] } = clientApi.widget.umami.getVisitorStats.useQuery( - { - integrationIds, - websiteId, - timeFrame, - eventName, - }, - { staleTime: 5 * 60 * 1000 }, + { integrationIds, websiteId, timeFrame, eventName }, + umamiQueryOptions, ); const activeVisitorsInput = { integrationId: integrationIds[0] ?? "", websiteId }; - const { data: activeVisitors = 0 } = clientApi.widget.umami.getActiveVisitors.useQuery(activeVisitorsInput, { - staleTime: 30 * 1000, - }); - const utils = clientApi.useUtils(); - clientApi.widget.umami.subscribeActiveVisitors.useSubscription(activeVisitorsInput, { - onData(count) { - utils.widget.umami.getActiveVisitors.setData(activeVisitorsInput, () => count); - }, - }); + const { data: activeVisitors = 0 } = clientApi.widget.umami.getActiveVisitors.useQuery( + activeVisitorsInput, + umamiQueryOptions, + ); const { data: multiEventSeries } = clientApi.widget.umami.getMultiEventTimeSeries.useQuery( { integrationId: integrationIds[0] ?? "", websiteId, timeFrame, eventNames: [...eventNames].toSorted() }, - { enabled: viewMode === "events" && eventNames.length > 0, staleTime: 5 * 60 * 1000 }, + { enabled: viewMode === "events" && eventNames.length > 0, ...umamiQueryOptions }, ); const multiEventTotal = multiEventSeries diff --git a/packages/widgets/src/umami/umami-events-content.tsx b/packages/widgets/src/umami/umami-events-content.tsx index 0351e906c..ef91df422 100644 --- a/packages/widgets/src/umami/umami-events-content.tsx +++ b/packages/widgets/src/umami/umami-events-content.tsx @@ -7,7 +7,7 @@ import { clientApi } from "@homarr/api/client"; import type { UmamiEventSeries } from "@homarr/integrations/types"; import { useScopedI18n } from "@homarr/translation/client"; -import { EVENT_COLORS, formatXLabel } from "./umami-utils"; +import { EVENT_COLORS, formatXLabel, umamiQueryOptions } from "./umami-utils"; interface UmamiEventsContentProps { integrationIds: string[]; @@ -29,13 +29,8 @@ export function UmamiEventsContent({ const tickColor = colorScheme === "dark" ? "#c1c2c5" : "#495057"; const { data: series = [] } = clientApi.widget.umami.getMultiEventTimeSeries.useQuery( - { - integrationId: integrationIds[0] ?? "", - websiteId, - timeFrame, - eventNames: [...eventNames].toSorted(), - }, - { staleTime: 5 * 60 * 1000 }, + { integrationId: integrationIds[0] ?? "", websiteId, timeFrame, eventNames: [...eventNames].toSorted() }, + umamiQueryOptions, ); if (eventNames.length === 0) { diff --git a/packages/widgets/src/umami/umami-top-list.tsx b/packages/widgets/src/umami/umami-top-list.tsx index 125115f0c..3ce4b8730 100644 --- a/packages/widgets/src/umami/umami-top-list.tsx +++ b/packages/widgets/src/umami/umami-top-list.tsx @@ -6,6 +6,8 @@ import { clientApi } from "@homarr/api/client"; import type { UmamiMetricItem } from "@homarr/integrations/types"; import { useScopedI18n } from "@homarr/translation/client"; +import { umamiQueryOptions } from "./umami-utils"; + interface UmamiTopListProps { integrationIds: string[]; websiteId: string; @@ -16,13 +18,8 @@ interface UmamiTopListProps { export function UmamiTopPagesContent({ integrationIds, websiteId, timeFrame, limit }: UmamiTopListProps) { const t = useScopedI18n("widget.umami"); const { data = [] } = clientApi.widget.umami.getTopPages.useQuery( - { - integrationId: integrationIds[0] ?? "", - websiteId, - timeFrame, - limit, - }, - { staleTime: 5 * 60 * 1000 }, + { integrationId: integrationIds[0] ?? "", websiteId, timeFrame, limit }, + umamiQueryOptions, ); return ; } @@ -30,13 +27,8 @@ export function UmamiTopPagesContent({ integrationIds, websiteId, timeFrame, lim export function UmamiTopReferrersContent({ integrationIds, websiteId, timeFrame, limit }: UmamiTopListProps) { const t = useScopedI18n("widget.umami"); const { data = [] } = clientApi.widget.umami.getTopReferrers.useQuery( - { - integrationId: integrationIds[0] ?? "", - websiteId, - timeFrame, - limit, - }, - { staleTime: 5 * 60 * 1000 }, + { integrationId: integrationIds[0] ?? "", websiteId, timeFrame, limit }, + umamiQueryOptions, ); return ( = { @@ -23,42 +22,23 @@ const statusColors: Record = { type UpsLayout = "mini" | "compact" | "full"; -export default function UpsWidget({ options, integrationIds, isEditMode, width }: WidgetComponentProps<"ups">) { +export default function UpsWidget({ options, integrationIds, width }: WidgetComponentProps<"ups">) { if (integrationIds.length === 0) { throw new NoIntegrationSelectedError(); } - return ; + return ; } interface UpsContentProps { integrationIds: string[]; options: WidgetComponentProps<"ups">["options"]; - isEditMode: boolean; width: number; } -function UpsContent({ integrationIds, options, isEditMode, width }: UpsContentProps) { +function UpsContent({ integrationIds, options, width }: UpsContentProps) { const t = useScopedI18n("widget.ups"); - const { data } = clientApi.widget.ups.getSummaries.useQuery({ integrationIds }, { staleTime: 30 * 1000 }); - - const utils = clientApi.useUtils(); - clientApi.widget.ups.subscribeSummaries.useSubscription( - { integrationIds }, - { - enabled: !isEditMode, - onData(newData) { - utils.widget.ups.getSummaries.setData({ integrationIds }, (prevData) => { - if (!prevData) return prevData; - return prevData.map((instance) => - instance.integrationId === newData.integrationId - ? { ...instance, summaries: newData.summaries, updatedAt: newData.timestamp } - : instance, - ); - }); - }, - }, - ); + const { data } = clientApi.widget.ups.getSummaries.useQuery({ integrationIds }); if (!data) return ; @@ -67,7 +47,7 @@ function UpsContent({ integrationIds, options, isEditMode, width }: UpsContentPr ); if (devices.length === 0) { - throw new NoIntegrationDataError(); + return ; } // Pick a layout from the available width so the widget stays useful at any size. The surrounding diff --git a/packages/widgets/src/uptime-kuma/component.tsx b/packages/widgets/src/uptime-kuma/component.tsx index 2a3b6136b..5719f2f13 100644 --- a/packages/widgets/src/uptime-kuma/component.tsx +++ b/packages/widgets/src/uptime-kuma/component.tsx @@ -85,51 +85,23 @@ const heroVariantByRing = { false: classes.heroTextOnly, } as const; -export default function UptimeKumaWidget({ - integrationIds, - isEditMode, - options, - width, -}: WidgetComponentProps<"uptimeKuma">) { +export default function UptimeKumaWidget({ integrationIds, options, width }: WidgetComponentProps<"uptimeKuma">) { if (integrationIds.length === 0) { throw new NoIntegrationDataError(); } - return ; + return ; } interface UptimeKumaContentProps { integrationIds: string[]; - isEditMode: boolean; options: WidgetComponentProps<"uptimeKuma">["options"]; width: number; } -function UptimeKumaContent({ integrationIds, isEditMode, options, width }: UptimeKumaContentProps) { +function UptimeKumaContent({ integrationIds, options, width }: UptimeKumaContentProps) { const t = useScopedI18n("widget.uptimeKuma"); - const { data: dashboardData } = clientApi.widget.uptimeKuma.getDashboard.useQuery( - { integrationIds }, - { staleTime: 30 * 1000 }, - ); - - const utils = clientApi.useUtils(); - clientApi.widget.uptimeKuma.subscribeToDashboard.useSubscription( - { integrationIds }, - { - enabled: !isEditMode, - onData(newData) { - utils.widget.uptimeKuma.getDashboard.setData({ integrationIds }, (prevData) => { - if (!prevData) return prevData; - return prevData.map((instance) => { - if (instance.integrationId === newData.integrationId) { - return { ...instance, dashboard: newData.dashboard, updatedAt: newData.timestamp }; - } - return instance; - }); - }); - }, - }, - ); + const { data: dashboardData } = clientApi.widget.uptimeKuma.getDashboard.useQuery({ integrationIds }); if (!dashboardData) return ; diff --git a/packages/widgets/src/vpn/component.tsx b/packages/widgets/src/vpn/component.tsx index 85ee4e3cf..79ed5d695 100644 --- a/packages/widgets/src/vpn/component.tsx +++ b/packages/widgets/src/vpn/component.tsx @@ -1,53 +1,20 @@ "use client"; -import React from "react"; import { Flex, ScrollArea, Stack } from "@mantine/core"; import { clientApi } from "@homarr/api/client"; import type { WidgetComponentProps } from "../definition"; -import { updateVpnInfoFromSubscription } from "./helpers"; import { VpnIntegrationCard } from "./vpn-card"; export default function VpnWidget({ options, integrationIds }: WidgetComponentProps<"vpn">) { - const [integrations] = clientApi.widget.vpn.getSummaries.useSuspenseQuery( - { - ...options, - integrationIds, - }, - { - // Initial load only. Live updates arrive via the subscription below, which is - // fed by the VPN cron job publishing to the integration channel. - refetchOnMount: false, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); - const utils = clientApi.useUtils(); - - clientApi.widget.vpn.subscribeSummaries.useSubscription( - { - integrationIds, - }, - { - onData: (data) => { - utils.widget.vpn.getSummaries.setData( - { - ...options, - integrationIds, - }, - (prevData) => updateVpnInfoFromSubscription(prevData, data), - ); - }, - }, - ); - - if (integrations.length === 1) { - // It will always have at least one integration as otherwise the NoIntegrationSelectedError would be thrown in item-content.tsx - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const vpn = integrations[0]!; + const { data: integrations = [] } = clientApi.widget.vpn.getSummaries.useQuery({ + ...options, + integrationIds, + }); + const [vpn] = integrations; + if (integrations.length === 1 && vpn) { return ( diff --git a/packages/widgets/src/vpn/helpers.ts b/packages/widgets/src/vpn/helpers.ts index 24a81720b..9bc801429 100644 --- a/packages/widgets/src/vpn/helpers.ts +++ b/packages/widgets/src/vpn/helpers.ts @@ -1,22 +1,5 @@ -import type { RouterOutputs } from "@homarr/api"; - export const RUNNING_STATUS = "running"; -type VpnInfoItem = RouterOutputs["widget"]["vpn"]["getSummaries"][number]; - export function getStatusColor(status: string) { return status === RUNNING_STATUS ? "green" : "red"; } - -export function updateVpnInfoFromSubscription( - prevData: VpnInfoItem[] | undefined, - data: { integration: { id: string }; summary: VpnInfoItem["summary"] }, -): VpnInfoItem[] | undefined { - if (!prevData) { - return undefined; - } - - return prevData.map((item) => - item.integration.id === data.integration.id ? { ...item, summary: data.summary } : item, - ); -} diff --git a/packages/widgets/src/weather/component.tsx b/packages/widgets/src/weather/component.tsx index a2e9778ca..6e76ef8f2 100644 --- a/packages/widgets/src/weather/component.tsx +++ b/packages/widgets/src/weather/component.tsx @@ -19,16 +19,7 @@ export default function WeatherWidget({ isEditMode, options }: WidgetComponentPr latitude: options.location.latitude, longitude: options.location.longitude, }; - const { data: weather } = clientApi.widget.weather.atLocation.useQuery(input, { - staleTime: 60 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - }); - - const utils = clientApi.useUtils(); - clientApi.widget.weather.subscribeAtLocation.useSubscription(input, { - onData: (data) => utils.widget.weather.atLocation.setData(input, data), - }); + const { data: weather } = clientApi.widget.weather.atLocation.useQuery(input); if (!weather) return ; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 682dce7d5..574aad9dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,11 +40,8 @@ catalogs: specifier: 7.2.1 version: 7.2.1 '@gfazioli/mantine-onboarding-tour': - specifier: ^4.0.5 - version: 4.0.5 - '@gitbeaker/rest': - specifier: ^43.8.0 - version: 43.8.0 + specifier: ^4.0.6 + version: 4.0.6 '@homarr/gridstack': specifier: ^1.12.0 version: 1.12.0 @@ -52,8 +49,8 @@ catalogs: specifier: ^2.6.0 version: 2.6.0 '@immich/sdk': - specifier: ^2.7.5 - version: 2.7.5 + specifier: ^3.0.1 + version: 3.0.1 '@jellyfin/sdk': specifier: ^0.13.0 version: 0.13.0 @@ -61,53 +58,50 @@ catalogs: specifier: ^1.4.0 version: 1.4.0 '@mantine/charts': - specifier: ^9.3.2 - version: 9.3.2 + specifier: ^9.4.1 + version: 9.4.1 '@mantine/colors-generator': - specifier: ^9.3.2 - version: 9.3.2 + specifier: ^9.4.1 + version: 9.4.1 '@mantine/core': - specifier: ^9.3.2 - version: 9.3.2 + specifier: ^9.4.1 + version: 9.4.1 '@mantine/dates': - specifier: ^9.3.2 - version: 9.3.2 + specifier: ^9.4.1 + version: 9.4.1 '@mantine/dropzone': - specifier: ^9.3.2 - version: 9.3.2 + specifier: ^9.4.1 + version: 9.4.1 '@mantine/form': - specifier: ^9.3.2 - version: 9.3.2 + specifier: ^9.4.1 + version: 9.4.1 '@mantine/hooks': - specifier: ^9.3.2 - version: 9.3.2 + specifier: ^9.4.1 + version: 9.4.1 '@mantine/modals': - specifier: ^9.3.2 - version: 9.3.2 + specifier: ^9.4.1 + version: 9.4.1 '@mantine/notifications': - specifier: ^9.3.2 - version: 9.3.2 + specifier: ^9.4.1 + version: 9.4.1 '@mantine/spotlight': - specifier: ^9.3.2 - version: 9.3.2 + specifier: ^9.4.1 + version: 9.4.1 '@mantine/tiptap': - specifier: ^9.3.2 - version: 9.3.2 + specifier: ^9.4.1 + version: 9.4.1 '@modelcontextprotocol/sdk': specifier: 1.29.0 version: 1.29.0 - '@octokit/auth-app': - specifier: ^8.2.0 - version: 8.2.0 '@paralleldrive/cuid2': specifier: ^3.3.0 version: 3.3.0 '@playwright/test': - specifier: ^1.61.0 - version: 1.61.0 + specifier: ^1.61.1 + version: 1.61.1 '@scalar/api-reference-react': - specifier: ^0.9.47 - version: 0.9.47 + specifier: ^0.9.51 + version: 0.9.52 '@semantic-release/changelog': specifier: ^6.0.3 version: 6.0.3 @@ -118,8 +112,8 @@ catalogs: specifier: ^10.0.1 version: 10.0.1 '@semantic-release/github': - specifier: ^12.0.8 - version: 12.0.8 + specifier: ^12.0.9 + version: 12.0.9 '@semantic-release/npm': specifier: ^13.1.5 version: 13.1.5 @@ -132,27 +126,30 @@ catalogs: '@tabler/icons-react': specifier: ^3.44.0 version: 3.44.0 - '@tanstack/query-persist-client-core': - specifier: ^5.101.0 - version: 5.101.0 + '@tanstack/query-async-storage-persister': + specifier: ^5.101.2 + version: 5.101.2 '@tanstack/react-query': - specifier: ^5.101.0 - version: 5.101.0 + specifier: ^5.101.2 + version: 5.101.2 '@tanstack/react-query-devtools': - specifier: ^5.101.0 - version: 5.101.0 + specifier: ^5.101.2 + version: 5.101.2 '@tanstack/react-query-next-experimental': - specifier: ^5.101.0 - version: 5.101.0 + specifier: ^5.101.2 + version: 5.101.2 + '@tanstack/react-query-persist-client': + specifier: ^5.101.2 + version: 5.101.2 '@testcontainers/mysql': - specifier: ^12.0.3 - version: 12.0.3 + specifier: ^12.0.4 + version: 12.0.4 '@testcontainers/postgresql': - specifier: ^12.0.3 - version: 12.0.3 + specifier: ^12.0.4 + version: 12.0.4 '@testcontainers/redis': - specifier: ^12.0.3 - version: 12.0.3 + specifier: ^12.0.4 + version: 12.0.4 '@tiptap/extension-color': specifier: 3.27.1 version: 3.27.1 @@ -202,23 +199,23 @@ catalogs: specifier: ^3.27.1 version: 3.27.1 '@trpc/client': - specifier: ^11.17.0 - version: 11.17.0 + specifier: ^11.18.0 + version: 11.18.0 '@trpc/next': - specifier: ^11.17.0 - version: 11.17.0 + specifier: ^11.18.0 + version: 11.18.0 '@trpc/react-query': - specifier: ^11.17.0 - version: 11.17.0 + specifier: ^11.18.0 + version: 11.18.0 '@trpc/server': - specifier: ^11.17.0 - version: 11.17.0 + specifier: ^11.18.0 + version: 11.18.0 '@trpc/tanstack-react-query': - specifier: ^11.17.0 - version: 11.17.0 + specifier: ^11.18.0 + version: 11.18.0 '@turbo/gen': - specifier: ^2.9.18 - version: 2.9.18 + specifier: ^2.10.2 + version: 2.10.2 '@types/adm-zip': specifier: 0.5.8 version: 0.5.8 @@ -271,8 +268,8 @@ catalogs: specifier: ^0.4.14 version: 0.4.14 '@vitejs/plugin-react': - specifier: ^6.0.2 - version: 6.0.2 + specifier: ^6.0.3 + version: 6.0.3 '@vitest/coverage-v8': specifier: ^4.1.9 version: 4.1.9 @@ -289,8 +286,8 @@ catalogs: specifier: ^6.0.0 version: 6.0.0 adm-zip: - specifier: 0.5.17 - version: 0.5.17 + specifier: 0.5.18 + version: 0.5.18 babel-plugin-react-compiler: specifier: ^1.0.0 version: 1.0.0 @@ -298,8 +295,8 @@ catalogs: specifier: ^6.0.0 version: 6.0.0 better-sqlite3: - specifier: ^12.10.0 - version: 12.10.0 + specifier: ^12.11.1 + version: 12.11.1 chroma-js: specifier: ^3.2.0 version: 3.2.0 @@ -310,11 +307,11 @@ catalogs: specifier: ^10.0.3 version: 10.0.3 conventional-changelog-conventionalcommits: - specifier: ^9.3.1 - version: 9.3.1 + specifier: ^10.2.0 + version: 10.2.0 cookie: - specifier: 1.1.1 - version: 1.1.1 + specifier: 2.0.1 + version: 2.0.1 cookies: specifier: ^0.9.1 version: 0.9.1 @@ -337,8 +334,8 @@ catalogs: specifier: ^0.2.9 version: 0.2.9 dockerode: - specifier: ^5.0.0 - version: 5.0.0 + specifier: ^5.0.1 + version: 5.0.1 dotenv: specifier: ^17.4.2 version: 17.4.2 @@ -361,8 +358,8 @@ catalogs: specifier: ^5.9.3 version: 5.9.3 fastify: - specifier: ^5.8.5 - version: 5.8.5 + specifier: ^5.9.0 + version: 5.9.0 flag-icons: specifier: ^7.5.0 version: 7.5.0 @@ -400,8 +397,8 @@ catalogs: specifier: ^4.5.0 version: 4.5.0 lucide-react: - specifier: ^1.21.0 - version: 1.21.0 + specifier: ^1.23.0 + version: 1.23.0 mantine-datatable: specifier: ^9.3.1 version: 9.3.1 @@ -418,14 +415,14 @@ catalogs: specifier: 3.22.5 version: 3.22.5 next: - specifier: 16.2.9 - version: 16.2.9 + specifier: 16.2.10 + version: 16.2.10 next-auth: specifier: 5.0.0-beta.31 version: 5.0.0-beta.31 next-intl: - specifier: 4.13.0 - version: 4.13.0 + specifier: 4.13.1 + version: 4.13.1 node-loader: specifier: ^2.1.0 version: 2.1.0 @@ -433,11 +430,11 @@ catalogs: specifier: ^5.0.5 version: 5.0.5 oxfmt: - specifier: ^0.55.0 - version: 0.55.0 + specifier: ^0.57.0 + version: 0.57.0 oxlint: - specifier: ^1.70.0 - version: 1.70.0 + specifier: ^1.72.0 + version: 1.72.0 pg: specifier: ^8.22.0 version: 8.22.0 @@ -445,11 +442,11 @@ catalogs: specifier: ^1.18.0 version: 1.18.0 posthog-js: - specifier: ^1.391.2 - version: 1.391.2 + specifier: ^1.396.4 + version: 1.396.5 posthog-node: - specifier: ^4.18.0 - version: 4.18.0 + specifier: ^5.39.2 + version: 5.39.4 prismjs: specifier: ^1.30.0 version: 1.30.0 @@ -478,8 +475,8 @@ catalogs: specifier: ^0.14.1 version: 0.14.1 recharts: - specifier: ^3.8.1 - version: 3.8.1 + specifier: ^3.9.1 + version: 3.9.1 sass: specifier: ^1.101.0 version: 1.101.0 @@ -499,8 +496,8 @@ catalogs: specifier: ^5.3.1 version: 5.3.1 testcontainers: - specifier: ^12.0.3 - version: 12.0.3 + specifier: ^12.0.4 + version: 12.0.4 timezones-ical-library: specifier: ^2.2.1 version: 2.2.1 @@ -511,20 +508,20 @@ catalogs: specifier: ^3.3.0 version: 3.3.0 tsdav: - specifier: ^2.2.2 - version: 2.2.2 + specifier: ^2.3.0 + version: 2.3.0 tsx: specifier: 4.20.4 version: 4.20.4 turbo: - specifier: ^2.9.18 - version: 2.9.18 + specifier: ^2.10.2 + version: 2.10.2 typescript: specifier: ^6.0.3 version: 6.0.3 undici: - specifier: 7.27.2 - version: 7.27.2 + specifier: 7.28.0 + version: 7.28.0 use-deep-compare-effect: specifier: ^1.8.1 version: 1.8.1 @@ -532,8 +529,8 @@ catalogs: specifier: ^8.23.9 version: 8.23.9 vite: - specifier: ^8.0.16 - version: 8.0.16 + specifier: ^8.1.2 + version: 8.1.3 vite-tsconfig-paths: specifier: ^6.1.1 version: 6.1.1 @@ -565,23 +562,23 @@ catalogs: overrides: '@babel/helpers@<7.26.10': '>=7.29.7' '@babel/runtime@<7.26.10': '>=7.29.7' - axios@>=1.0.0 <1.8.2: '>=1.18.0' - brace-expansion@>=2.0.0 <=2.0.1: '>=5.0.6' - brace-expansion@>=1.0.0 <=1.1.11: '>=5.0.6' - esbuild@<=0.24.2: '>=0.28.0' + axios@>=1.0.0 <1.8.2: '>=1.18.1' + brace-expansion@>=2.0.0 <=2.0.1: '>=5.0.7' + brace-expansion@>=1.0.0 <=1.1.11: '>=5.0.7' + esbuild@<=0.24.2: '>=0.28.1' form-data@>=4.0.0 <4.0.4: '>=4.0.6' - hono@<4.6.5: '>=4.12.26' + hono@<4.6.5: '>=4.12.27' linkifyjs@<4.3.2: '>=4.3.3' - nanoid@>=4.0.0 <5.0.9: '>=5.1.15' + nanoid@>=4.0.0 <5.0.9: '>=5.1.16' prismjs@<1.30.0: '>=1.30.0' - proxmox-api>undici: 7.27.1 + proxmox-api>undici: 7.28.0 react-is: ^19.2.7 rollup@>=4.0.0 <4.22.4: '>=4.62.2' sha.js@<=2.4.11: '>=2.4.12' - tar-fs@>=3.0.0 <3.0.9: '>=3.1.2' - tar-fs@>=2.0.0 <2.1.3: '>=3.1.2' + tar-fs@>=3.0.0 <3.0.9: '>=3.1.3' + tar-fs@>=2.0.0 <2.1.3: '>=3.1.3' tmp@<=0.2.3: '>=0.2.7' - vite@>=5.0.0 <=5.4.18: '>=8.0.16' + vite@>=5.0.0 <=5.4.18: '>=8.1.3' patchedDependencies: '@types/node-unifi': 5e6ae51e2a17a7f9729bfa30b0eb3d0842a5810ac6db47603ab4a6efa1ed84c5 @@ -597,7 +594,7 @@ importers: devDependencies: '@playwright/test': specifier: 'catalog:' - version: 1.61.0 + version: 1.61.1 '@semantic-release/changelog': specifier: 'catalog:' version: 6.0.3(semantic-release@25.0.5(typescript@6.0.3)) @@ -609,7 +606,7 @@ importers: version: 10.0.1(semantic-release@25.0.5(typescript@6.0.3)) '@semantic-release/github': specifier: 'catalog:' - version: 12.0.8(semantic-release@25.0.5(typescript@6.0.3)) + version: 12.0.9(semantic-release@25.0.5(typescript@6.0.3)) '@semantic-release/npm': specifier: 'catalog:' version: 13.1.5(semantic-release@25.0.5(typescript@6.0.3)) @@ -618,13 +615,13 @@ importers: version: 14.1.1(semantic-release@25.0.5(typescript@6.0.3)) '@testcontainers/redis': specifier: 'catalog:' - version: 12.0.3 + version: 12.0.4 '@turbo/gen': specifier: 'catalog:' - version: 2.9.18(@types/node@24.13.2) + version: 2.10.2(@types/node@24.13.2) '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.2(babel-plugin-react-compiler@1.0.0)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.15))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)) + version: 6.0.3(babel-plugin-react-compiler@1.0.0)(vite@8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.9(vitest@4.1.9) @@ -633,7 +630,7 @@ importers: version: 4.1.9(vitest@4.1.9) conventional-changelog-conventionalcommits: specifier: 'catalog:' - version: 9.3.1 + version: 10.2.0 cross-env: specifier: 'catalog:' version: 10.1.0 @@ -645,55 +642,55 @@ importers: version: 2.2.3 oxfmt: specifier: 'catalog:' - version: 0.55.0 + version: 0.57.0 oxlint: specifier: 'catalog:' - version: 1.70.0 + version: 1.72.0 semantic-release: specifier: 'catalog:' version: 25.0.5(typescript@6.0.3) testcontainers: specifier: 'catalog:' - version: 12.0.3 + version: 12.0.4 turbo: specifier: 'catalog:' - version: 2.9.18 + version: 2.10.2 typescript: specifier: 'catalog:' version: 6.0.3 vite: specifier: 'catalog:' - version: 8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.15))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0) + version: 8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0) vite-tsconfig-paths: specifier: 'catalog:' - version: 6.1.1(typescript@6.0.3)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.15))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)) + version: 6.1.1(typescript@6.0.3)(vite@8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)) vitest: specifier: 'catalog:' - version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@24.13.2)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.15))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@24.13.2)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)) apps/docs: dependencies: '@docusaurus/core': specifier: ^3.10.1 - version: 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + version: 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) '@docusaurus/faster': specifier: ^3.10.1 - version: 3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15) + version: 3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16) '@docusaurus/plugin-content-docs': specifier: ^3.10.1 - version: 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + version: 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) '@docusaurus/plugin-sitemap': specifier: ^3.10.1 - version: 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + version: 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) '@docusaurus/preset-classic': specifier: ^3.10.1 - version: 3.10.1(@algolia/client-search@5.53.0)(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/react@19.2.17)(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)(typescript@6.0.3) + version: 3.10.1(@algolia/client-search@5.53.0)(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/react@19.2.17)(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)(typescript@6.0.3) '@docusaurus/theme-common': specifier: ^3.10.1 - version: 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@docusaurus/theme-mermaid': specifier: ^3.10.1 - version: 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + version: 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) '@homarr/definitions': specifier: workspace:^ version: link:../../packages/definitions @@ -705,16 +702,16 @@ importers: version: 0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@scalar/api-reference-react': specifier: 'catalog:' - version: 0.9.47(nprogress@0.2.0)(react@19.2.7)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3) + version: 0.9.52(nprogress@0.2.0)(react@19.2.7)(tailwindcss@4.3.2)(typescript@6.0.3)(zod@4.4.3) '@signalwire/docusaurus-plugin-llms-txt': specifier: ^1.2.2 - version: 1.2.2(@docusaurus/core@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)) + version: 1.2.2(@docusaurus/core@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)) '@tabler/icons-react': specifier: 'catalog:' version: 3.44.0(react@19.2.7) '@tailwindcss/postcss': - specifier: ^4.3.1 - version: 4.3.1 + specifier: ^4.3.2 + version: 4.3.2 clsx: specifier: 'catalog:' version: 2.1.1 @@ -723,13 +720,13 @@ importers: version: 1.11.21 docusaurus-plugin-image-zoom: specifier: ^3.0.1 - version: 3.0.1(@docusaurus/theme-classic@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/react@19.2.17)(esbuild@0.28.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)) + version: 3.0.1(@docusaurus/theme-classic@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)) leader-line-new: specifier: ^1.1.9 version: 1.1.9 postcss: - specifier: ^8.5.15 - version: 8.5.15 + specifier: ^8.5.16 + version: 8.5.16 posthog-docusaurus: specifier: ^2.0.5 version: 2.0.5 @@ -746,21 +743,21 @@ importers: specifier: 'catalog:' version: 19.2.7(react@19.2.7) swiper: - specifier: ^12.2.0 - version: 12.2.0 + specifier: ^14.0.1 + version: 14.0.1 tailwindcss: - specifier: ^4.3.1 - version: 4.3.1 + specifier: ^4.3.2 + version: 4.3.2 devDependencies: '@docusaurus/module-type-aliases': specifier: ^3.10.1 - version: 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@docusaurus/tsconfig': specifier: ^3.10.1 version: 3.10.1 '@docusaurus/types': specifier: ^3.10.1 - version: 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@fec/remark-a11y-emoji': specifier: ^4.0.2 version: 4.0.2 @@ -799,7 +796,7 @@ importers: version: 3.2.2(react@19.2.7) '@gfazioli/mantine-onboarding-tour': specifier: 'catalog:' - version: 4.0.5(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(@tabler/icons-react@3.44.0(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 4.0.6(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(@tabler/icons-react@3.44.0(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@homarr/analytics': specifier: workspace:^0.1.0 version: link:../../packages/analytics @@ -895,55 +892,58 @@ importers: version: link:../../packages/widgets '@mantine/colors-generator': specifier: 'catalog:' - version: 9.3.2(chroma-js@3.2.0) + version: 9.4.1(chroma-js@3.2.0) '@mantine/core': specifier: 'catalog:' - version: 9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@mantine/dropzone': specifier: 'catalog:' - version: 9.3.2(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 9.4.1(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@mantine/hooks': specifier: 'catalog:' - version: 9.3.2(react@19.2.7) + version: 9.4.1(react@19.2.7) '@mantine/modals': specifier: 'catalog:' - version: 9.3.2(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 9.4.1(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@mantine/tiptap': specifier: 'catalog:' - version: 9.3.2(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(@tiptap/extension-link@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1))(@tiptap/react@3.27.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 9.4.1(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(@tiptap/extension-link@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1))(@tiptap/react@3.27.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@modelcontextprotocol/sdk': specifier: 'catalog:' version: 1.29.0(zod@4.4.3) '@scalar/api-reference-react': specifier: 'catalog:' - version: 0.9.47(nprogress@0.2.0)(react@19.2.7)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3) + version: 0.9.52(nprogress@0.2.0)(react@19.2.7)(tailwindcss@4.3.2)(typescript@6.0.3)(zod@4.4.3) '@tabler/icons-react': specifier: 'catalog:' version: 3.44.0(react@19.2.7) - '@tanstack/query-persist-client-core': + '@tanstack/query-async-storage-persister': specifier: 'catalog:' - version: 5.101.0 + version: 5.101.2 '@tanstack/react-query': specifier: 'catalog:' - version: 5.101.0(react@19.2.7) + version: 5.101.2(react@19.2.7) '@tanstack/react-query-devtools': specifier: 'catalog:' - version: 5.101.0(@tanstack/react-query@5.101.0(react@19.2.7))(react@19.2.7) + version: 5.101.2(@tanstack/react-query@5.101.2(react@19.2.7))(react@19.2.7) '@tanstack/react-query-next-experimental': specifier: 'catalog:' - version: 5.101.0(@tanstack/react-query@5.101.0(react@19.2.7))(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(react@19.2.7) + version: 5.101.2(@tanstack/react-query@5.101.2(react@19.2.7))(next@16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(react@19.2.7) + '@tanstack/react-query-persist-client': + specifier: 'catalog:' + version: 5.101.2(@tanstack/react-query@5.101.2(react@19.2.7))(react@19.2.7) '@trpc/client': specifier: 'catalog:' - version: 11.17.0(@trpc/server@11.17.0(typescript@6.0.3))(typescript@6.0.3) + version: 11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3) '@trpc/next': specifier: 'catalog:' - version: 11.17.0(@tanstack/react-query@5.101.0(react@19.2.7))(@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/react-query@11.17.0(@tanstack/react-query@5.101.0(react@19.2.7))(@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/server@11.17.0(typescript@6.0.3))(react@19.2.7)(typescript@6.0.3))(@trpc/server@11.17.0(typescript@6.0.3))(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + version: 11.18.0(@tanstack/react-query@5.101.2(react@19.2.7))(@trpc/client@11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/react-query@11.18.0(@tanstack/react-query@5.101.2(react@19.2.7))(@trpc/client@11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/server@11.18.0(typescript@6.0.3))(react@19.2.7)(typescript@6.0.3))(@trpc/server@11.18.0(typescript@6.0.3))(next@16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) '@trpc/react-query': specifier: 'catalog:' - version: 11.17.0(@tanstack/react-query@5.101.0(react@19.2.7))(@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/server@11.17.0(typescript@6.0.3))(react@19.2.7)(typescript@6.0.3) + version: 11.18.0(@tanstack/react-query@5.101.2(react@19.2.7))(@trpc/client@11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/server@11.18.0(typescript@6.0.3))(react@19.2.7)(typescript@6.0.3) '@trpc/server': specifier: 'catalog:' - version: 11.17.0(typescript@6.0.3) + version: 11.18.0(typescript@6.0.3) '@xterm/addon-canvas': specifier: 'catalog:' version: 0.7.0(@xterm/xterm@6.0.0) @@ -955,13 +955,13 @@ importers: version: 6.0.0 adm-zip: specifier: 'catalog:' - version: 0.5.17 + version: 0.5.18 babel-plugin-react-compiler: specifier: 'catalog:' version: 1.0.0 better-sqlite3: specifier: 'catalog:' - version: 12.10.0 + version: 12.11.1 chroma-js: specifier: 'catalog:' version: 3.2.0 @@ -976,7 +976,7 @@ importers: version: 17.4.2 drizzle-orm: specifier: 'catalog:' - version: 0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1) + version: 0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1) flag-icons: specifier: 'catalog:' version: 7.5.0 @@ -994,25 +994,25 @@ importers: version: 3.10.1 lucide-react: specifier: 'catalog:' - version: 1.21.0(react@19.2.7) + version: 1.23.0(react@19.2.7) mantine-datatable: specifier: 'catalog:' - version: 9.3.1(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(clsx@2.1.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 9.3.1(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(clsx@2.1.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) mantine-react-table: specifier: 'catalog:' - version: mantine-react-table-open@9.0.3(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/dates@9.3.2(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(@tabler/icons-react@3.44.0(react@19.2.7))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: mantine-react-table-open@9.0.3(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/dates@9.4.1(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(@tabler/icons-react@3.44.0(react@19.2.7))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) mcp-handler: specifier: 'catalog:' - version: 1.1.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)) + version: 1.1.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(next@16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)) next: specifier: 'catalog:' - version: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) + version: 16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) postcss-preset-mantine: specifier: 'catalog:' - version: 1.18.0(postcss@8.5.15) + version: 1.18.0(postcss@8.5.16) posthog-js: specifier: 'catalog:' - version: 1.391.2 + version: 1.396.5 prismjs: specifier: 'catalog:' version: 1.30.0 @@ -1039,7 +1039,7 @@ importers: version: 2.2.6 trpc-to-mcp: specifier: 'catalog:' - version: 1.3.2(@trpc/server@11.17.0(typescript@6.0.3))(better-auth@1.6.15(@opentelemetry/api@1.9.0)(better-sqlite3@12.10.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1))(mysql2@3.22.5(@types/node@24.13.2))(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(pg@8.22.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)(vue@3.5.35(typescript@6.0.3)))(mcp-handler@1.1.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)))(zod@4.4.3) + version: 1.3.2(@trpc/server@11.18.0(typescript@6.0.3))(better-auth@1.6.15(@opentelemetry/api@1.9.0)(better-sqlite3@12.11.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1))(mysql2@3.22.5(@types/node@24.13.2))(next@16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(pg@8.22.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)(vue@3.5.35(typescript@6.0.3)))(mcp-handler@1.1.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(next@16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)))(zod@4.4.3) use-deep-compare-effect: specifier: 'catalog:' version: 1.8.1(react@19.2.7) @@ -1079,7 +1079,7 @@ importers: version: 10.0.3 node-loader: specifier: 'catalog:' - version: 2.1.0(webpack@5.107.1(esbuild@0.28.0)(postcss@8.5.15)) + version: 2.1.0(webpack@5.107.1(esbuild@0.28.1)(postcss@8.5.16)) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1145,13 +1145,13 @@ importers: version: 17.4.2 fastify: specifier: 'catalog:' - version: 5.8.5 + version: 5.9.0 superjson: specifier: 'catalog:' version: 2.2.6 undici: specifier: 'catalog:' - version: 7.27.2 + version: 7.28.0 devDependencies: '@homarr/tsconfig': specifier: workspace:^0.1.0 @@ -1240,7 +1240,7 @@ importers: version: link:../docker posthog-node: specifier: 'catalog:' - version: 4.18.0 + version: 5.39.4(rxjs@7.8.2) devDependencies: '@homarr/tsconfig': specifier: workspace:^0.1.0 @@ -1313,19 +1313,19 @@ importers: version: 1.4.0 '@tanstack/react-query': specifier: 'catalog:' - version: 5.101.0(react@19.2.7) + version: 5.101.2(react@19.2.7) '@trpc/client': specifier: 'catalog:' - version: 11.17.0(@trpc/server@11.17.0(typescript@6.0.3))(typescript@6.0.3) + version: 11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3) '@trpc/react-query': specifier: 'catalog:' - version: 11.17.0(@tanstack/react-query@5.101.0(react@19.2.7))(@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/server@11.17.0(typescript@6.0.3))(react@19.2.7)(typescript@6.0.3) + version: 11.18.0(@tanstack/react-query@5.101.2(react@19.2.7))(@trpc/client@11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/server@11.18.0(typescript@6.0.3))(react@19.2.7)(typescript@6.0.3) '@trpc/server': specifier: 'catalog:' - version: 11.17.0(typescript@6.0.3) + version: 11.18.0(typescript@6.0.3) '@trpc/tanstack-react-query': specifier: 'catalog:' - version: 11.17.0(@tanstack/react-query@5.101.0(react@19.2.7))(@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/server@11.17.0(typescript@6.0.3))(react@19.2.7)(typescript@6.0.3) + version: 11.18.0(@tanstack/react-query@5.101.2(react@19.2.7))(@trpc/client@11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/server@11.18.0(typescript@6.0.3))(react@19.2.7)(typescript@6.0.3) jsonpath-plus: specifier: 'catalog:' version: 10.4.0 @@ -1334,7 +1334,7 @@ importers: version: 4.5.0 next: specifier: 'catalog:' - version: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) + version: 16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) react: specifier: 'catalog:' version: 19.2.7 @@ -1346,10 +1346,10 @@ importers: version: 2.2.6 trpc-to-mcp: specifier: 'catalog:' - version: 1.3.2(@trpc/server@11.17.0(typescript@6.0.3))(better-auth@1.6.15(@opentelemetry/api@1.9.0)(better-sqlite3@12.10.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1))(mysql2@3.22.5(@types/node@24.13.2))(next@16.2.9(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(pg@8.22.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)(vue@3.5.35(typescript@6.0.3)))(mcp-handler@1.1.0(@modelcontextprotocol/sdk@1.26.0(zod@4.4.3))(next@16.2.9(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)))(zod@4.4.3) + version: 1.3.2(@trpc/server@11.18.0(typescript@6.0.3))(better-auth@1.6.15(@opentelemetry/api@1.9.0)(better-sqlite3@12.11.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1))(mysql2@3.22.5(@types/node@24.13.2))(next@16.2.10(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(pg@8.22.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)(vue@3.5.35(typescript@6.0.3)))(mcp-handler@1.1.0(@modelcontextprotocol/sdk@1.26.0(zod@4.4.3))(next@16.2.10(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)))(zod@4.4.3) trpc-to-openapi: specifier: 'catalog:' - version: 3.3.0(patch_hash=2ca3c16af0fcca0c736697ad4fe553a14f794524fa9ce0d5c3e8ee4aea76090c)(@trpc/server@11.17.0(typescript@6.0.3))(zod-openapi@5.3.0(zod@4.4.3))(zod@4.4.3) + version: 3.3.0(patch_hash=2ca3c16af0fcca0c736697ad4fe553a14f794524fa9ce0d5c3e8ee4aea76090c)(@trpc/server@11.18.0(typescript@6.0.3))(zod-openapi@5.3.0(zod@4.4.3))(zod@4.4.3) zod: specifier: 'catalog:' version: 4.4.3 @@ -1395,10 +1395,10 @@ importers: version: 8.1.8 next: specifier: 'catalog:' - version: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) + version: 16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) next-auth: specifier: 'catalog:' - version: 5.0.0-beta.31(next@16.2.9(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(react@19.2.7) + version: 5.0.0-beta.31(next@16.2.10(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1482,13 +1482,13 @@ importers: version: 3.3.0 cookie: specifier: 'catalog:' - version: 1.1.1 + version: 2.0.1 dayjs: specifier: 'catalog:' version: 1.11.21 next: specifier: 'catalog:' - version: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) + version: 16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) octokit: specifier: 'catalog:' version: 5.0.5 @@ -1500,7 +1500,7 @@ importers: version: 19.2.7(react@19.2.7) undici: specifier: 'catalog:' - version: 7.27.2 + version: 7.28.0 zod: specifier: 'catalog:' version: 4.4.3 @@ -1522,13 +1522,13 @@ importers: version: 0.13.11(arktype@2.1.20)(typescript@6.0.3)(zod@4.4.3) better-sqlite3: specifier: 'catalog:' - version: 12.10.0 + version: 12.11.1 dns-caching: specifier: 'catalog:' version: 0.2.9 drizzle-orm: specifier: 'catalog:' - version: 0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1) + version: 0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1) ioredis: specifier: 'catalog:' version: 5.11.1 @@ -1682,19 +1682,19 @@ importers: version: link:../validation '@mantine/core': specifier: 'catalog:' - version: 9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@paralleldrive/cuid2': specifier: 'catalog:' version: 3.3.0 '@testcontainers/mysql': specifier: 'catalog:' - version: 12.0.3 + version: 12.0.4 '@testcontainers/postgresql': specifier: 'catalog:' - version: 12.0.3 + version: 12.0.4 better-sqlite3: specifier: 'catalog:' - version: 12.10.0 + version: 12.11.1 dotenv: specifier: 'catalog:' version: 17.4.2 @@ -1703,10 +1703,10 @@ importers: version: 0.31.10 drizzle-orm: specifier: 'catalog:' - version: 0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1) + version: 0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1) drizzle-zod: specifier: 'catalog:' - version: 0.8.3(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1))(zod@4.4.3) + version: 0.8.3(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1))(zod@4.4.3) mysql2: specifier: 'catalog:' version: 3.22.5(@types/node@24.13.2) @@ -1771,7 +1771,7 @@ importers: version: link:../core dockerode: specifier: 'catalog:' - version: 5.0.0 + version: 5.0.1 devDependencies: '@homarr/tsconfig': specifier: workspace:^0.1.0 @@ -1796,7 +1796,7 @@ importers: version: link:../validation '@mantine/form': specifier: 'catalog:' - version: 9.3.2(react@19.2.7) + version: 9.4.1(react@19.2.7) zod: specifier: 'catalog:' version: 4.4.3 @@ -1836,7 +1836,7 @@ importers: version: link:../validation '@mantine/core': specifier: 'catalog:' - version: 9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1906,9 +1906,6 @@ importers: '@ctrl/transmission': specifier: 'catalog:' version: 7.4.0 - '@gitbeaker/rest': - specifier: 'catalog:' - version: 43.8.0 '@homarr/common': specifier: workspace:^0.1.0 version: link:../common @@ -1926,7 +1923,7 @@ importers: version: link:../image-proxy '@homarr/node-unifi': specifier: 'catalog:' - version: 2.6.0(undici@7.27.2) + version: 2.6.0(undici@7.28.0) '@homarr/redis': specifier: workspace:^0.1.0 version: link:../redis @@ -1938,13 +1935,10 @@ importers: version: link:../validation '@immich/sdk': specifier: 'catalog:' - version: 2.7.5 + version: 3.0.1 '@jellyfin/sdk': specifier: 'catalog:' version: 0.13.0(axios@1.15.0) - '@octokit/auth-app': - specifier: 'catalog:' - version: 8.2.0 ical.js: specifier: 'catalog:' version: 2.2.1 @@ -1962,10 +1956,10 @@ importers: version: 2.2.1 tsdav: specifier: 'catalog:' - version: 2.2.2 + version: 2.3.0 undici: specifier: 'catalog:' - version: 7.27.2 + version: 7.28.0 ws: specifier: 'catalog:' version: 8.21.0 @@ -2002,10 +1996,10 @@ importers: version: link:../ui '@mantine/core': specifier: 'catalog:' - version: 9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@mantine/hooks': specifier: 'catalog:' - version: 9.3.2(react@19.2.7) + version: 9.4.1(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -2054,7 +2048,7 @@ importers: version: link:../validation '@mantine/core': specifier: 'catalog:' - version: 9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@tabler/icons-react': specifier: 'catalog:' version: 3.44.0(react@19.2.7) @@ -2063,7 +2057,7 @@ importers: version: 1.11.21 next: specifier: 'catalog:' - version: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) + version: 16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) react: specifier: 'catalog:' version: 19.2.7 @@ -2088,7 +2082,7 @@ importers: version: link:../ui '@mantine/notifications': specifier: 'catalog:' - version: 9.3.2(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 9.4.1(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@tabler/icons-react': specifier: 'catalog:' version: 3.44.0(react@19.2.7) @@ -2137,16 +2131,16 @@ importers: version: link:../validation '@mantine/core': specifier: 'catalog:' - version: 9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@mantine/hooks': specifier: 'catalog:' - version: 9.3.2(react@19.2.7) + version: 9.4.1(react@19.2.7) adm-zip: specifier: 'catalog:' - version: 0.5.17 + version: 0.5.18 next: specifier: 'catalog:' - version: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) + version: 16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) react: specifier: 'catalog:' version: 19.2.7 @@ -2270,7 +2264,7 @@ importers: version: 2.2.6 undici: specifier: 'catalog:' - version: 7.27.2 + version: 7.28.0 zod: specifier: 'catalog:' version: 4.4.3 @@ -2308,10 +2302,10 @@ importers: version: link:../server-settings '@mantine/dates': specifier: 'catalog:' - version: 9.3.2(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 9.4.1(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next: specifier: 'catalog:' - version: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) + version: 16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) react: specifier: 'catalog:' version: 19.2.7 @@ -2363,13 +2357,13 @@ importers: version: link:../ui '@mantine/core': specifier: 'catalog:' - version: 9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@mantine/hooks': specifier: 'catalog:' - version: 9.3.2(react@19.2.7) + version: 9.4.1(react@19.2.7) '@mantine/spotlight': specifier: 'catalog:' - version: 9.3.2(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 9.4.1(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@tabler/icons-react': specifier: 'catalog:' version: 3.44.0(react@19.2.7) @@ -2378,7 +2372,7 @@ importers: version: 2.20.1(@babel/core@7.29.0)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7) next: specifier: 'catalog:' - version: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) + version: 16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) react: specifier: 'catalog:' version: 19.2.7 @@ -2412,13 +2406,13 @@ importers: version: 4.3.1 mantine-react-table: specifier: 'catalog:' - version: mantine-react-table-open@9.0.3(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/dates@9.3.2(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(@tabler/icons-react@3.44.0(react@19.2.7))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: mantine-react-table-open@9.0.3(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/dates@9.4.1(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(@tabler/icons-react@3.44.0(react@19.2.7))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next: specifier: 'catalog:' - version: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) + version: 16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) next-intl: specifier: 'catalog:' - version: 4.13.0(next@16.2.9(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(react@19.2.7)(typescript@6.0.3) + version: 4.13.1(next@16.2.10(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(react@19.2.7)(typescript@6.0.3) react: specifier: 'catalog:' version: 19.2.7 @@ -2452,13 +2446,13 @@ importers: version: link:../validation '@mantine/core': specifier: 'catalog:' - version: 9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@mantine/dates': specifier: 'catalog:' - version: 9.3.2(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 9.4.1(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@mantine/hooks': specifier: 'catalog:' - version: 9.3.2(react@19.2.7) + version: 9.4.1(react@19.2.7) '@tabler/icons-react': specifier: 'catalog:' version: 3.44.0(react@19.2.7) @@ -2467,10 +2461,10 @@ importers: version: 4.2.0 mantine-react-table: specifier: 'catalog:' - version: mantine-react-table-open@9.0.3(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/dates@9.3.2(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(@tabler/icons-react@3.44.0(react@19.2.7))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: mantine-react-table-open@9.0.3(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/dates@9.4.1(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(@tabler/icons-react@3.44.0(react@19.2.7))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next: specifier: 'catalog:' - version: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) + version: 16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) react: specifier: 'catalog:' version: 19.2.7 @@ -2589,16 +2583,16 @@ importers: version: link:../validation '@mantine/charts': specifier: 'catalog:' - version: 9.3.2(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(recharts@3.8.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)) + version: 9.4.1(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(recharts@3.9.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)) '@mantine/core': specifier: 'catalog:' - version: 9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@mantine/form': specifier: 'catalog:' - version: 9.3.2(react@19.2.7) + version: 9.4.1(react@19.2.7) '@mantine/hooks': specifier: 'catalog:' - version: 9.3.2(react@19.2.7) + version: 9.4.1(react@19.2.7) '@tabler/icons-react': specifier: 'catalog:' version: 3.44.0(react@19.2.7) @@ -2661,16 +2655,16 @@ importers: version: 10.4.0 lucide-react: specifier: 'catalog:' - version: 1.21.0(react@19.2.7) + version: 1.23.0(react@19.2.7) mantine-datatable: specifier: 'catalog:' - version: 9.3.1(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(clsx@2.1.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 9.3.1(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(clsx@2.1.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) mantine-react-table: specifier: 'catalog:' - version: mantine-react-table-open@9.0.3(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/dates@9.3.2(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(@tabler/icons-react@3.44.0(react@19.2.7))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: mantine-react-table-open@9.0.3(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/dates@9.4.1(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(@tabler/icons-react@3.44.0(react@19.2.7))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next: specifier: 'catalog:' - version: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) + version: 16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) react: specifier: 'catalog:' version: 19.2.7 @@ -2688,7 +2682,7 @@ importers: version: 3.8.3(quill-delta@5.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) recharts: specifier: 'catalog:' - version: 3.8.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1) + version: 3.9.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1) video.js: specifier: 'catalog:' version: 8.23.9 @@ -3614,6 +3608,10 @@ packages: resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} engines: {node: '>=0.1.90'} + '@conventional-changelog/template@1.2.0': + resolution: {integrity: sha512-12qHxvlKjHmP0PQ+17EREgC7lWyLwbph1RKcZQZ7k7ZWGmrxfxC9gadHGfvzr0g0u8BhiBGg3tks93txodlyRQ==} + engines: {node: '>=22'} + '@csstools/cascade-layer-name-parser@2.0.5': resolution: {integrity: sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A==} engines: {node: '>=18'} @@ -4242,17 +4240,11 @@ packages: '@drizzle-team/brocli@0.12.0': resolution: {integrity: sha512-mlUE+rZ8CatQekLhnaiN91Iemdd+e2gFKooGlnRB3oPTL3VghLfX24dx7HrzMNeC1JrIB/0kpsfyty3f5HNfxQ==} - '@emnapi/core@1.10.0': - resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} - '@emnapi/core@1.11.0': - resolution: {integrity: sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==} - - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} '@emnapi/wasi-threads@1.2.2': resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} @@ -4286,6 +4278,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.25.12': resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} @@ -4304,6 +4302,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.25.12': resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} @@ -4322,6 +4326,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.25.12': resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} @@ -4340,6 +4350,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.25.12': resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} @@ -4358,6 +4374,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.25.12': resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} @@ -4376,6 +4398,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.25.12': resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} @@ -4394,6 +4422,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.12': resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} @@ -4412,6 +4446,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.25.12': resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} @@ -4430,6 +4470,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.25.12': resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} @@ -4448,6 +4494,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.25.12': resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} @@ -4466,6 +4518,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.25.12': resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} @@ -4484,6 +4542,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.25.12': resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} @@ -4502,6 +4566,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.25.12': resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} @@ -4520,6 +4590,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.25.12': resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} @@ -4538,6 +4614,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.25.12': resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} @@ -4556,6 +4638,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.25.12': resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} @@ -4574,6 +4662,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.12': resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} @@ -4592,6 +4686,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.12': resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} @@ -4610,6 +4710,12 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} @@ -4628,6 +4734,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.12': resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} @@ -4646,6 +4758,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} @@ -4664,6 +4782,12 @@ packages: cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.25.12': resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} @@ -4682,6 +4806,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.25.12': resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} @@ -4700,6 +4830,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.25.12': resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} @@ -4718,6 +4854,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.25.12': resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} @@ -4736,6 +4878,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@exodus/bytes@1.15.0': resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -4819,8 +4967,8 @@ packages: '@formatjs/intl-localematcher@0.8.1': resolution: {integrity: sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA==} - '@gfazioli/mantine-onboarding-tour@4.0.5': - resolution: {integrity: sha512-KyzmbcUqnUuezGZKIpsb6zeTlLgjetBQksysfYhuP8wfuqObkAL+aD0DnHnaxrdHxR9GYFkiIDImhHDUG1p5Vw==} + '@gfazioli/mantine-onboarding-tour@4.0.6': + resolution: {integrity: sha512-EMXwq5VPdQO8ppgKGY3i/KbB3jfuqJir5xmbAx0RNV3Ua3uXfznXJb0mNLykP1gQ+qe6WceORNsFUtQS0tIZAw==} peerDependencies: '@mantine/core': '>=9.0.0' '@mantine/hooks': '>=9.0.0' @@ -4828,18 +4976,6 @@ packages: react: ^18.x || ^19.x react-dom: ^18.x || ^19.x - '@gitbeaker/core@43.8.0': - resolution: {integrity: sha512-H+LfKuf4dExBinb79c+CXViRBvTVQNf5BYLNSizm2SiqdED5JruhKX88payefleY0szp7G/mySlFSXPyGRH1dQ==} - engines: {node: '>=18.20.0'} - - '@gitbeaker/requester-utils@43.8.0': - resolution: {integrity: sha512-d/SiJdxijc+aH5ZBQOw83XLxNSXqsBZNm5k3nPu1EHxGxK0fajXmxdMl0/vNXbKRggnIquFCxURkrQSEzfjqxQ==} - engines: {node: '>=18.20.0'} - - '@gitbeaker/rest@43.8.0': - resolution: {integrity: sha512-xxqsNsUXaFang9b2e/NTIgqUeuUlifA2Opy1mOVqTDuJZZNIOTgUNyziwBJoleBhMC0XuvY3JNVMWthufcVjRw==} - engines: {node: '>=18.20.0'} - '@grpc/grpc-js@1.12.5': resolution: {integrity: sha512-d3iiHxdpg5+ZcJ6jnDSOT8Z0O0VMVGy34jAnYLUX8yd36b1qn8f1TwOA/Lc7TsOh03IkPJ38eGI5qD2EjNkoEA==} engines: {node: '>=12.10.0'} @@ -4881,7 +5017,7 @@ packages: resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} engines: {node: '>=18.14.1'} peerDependencies: - hono: '>=4.12.26' + hono: '>=4.12.27' '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} @@ -5042,8 +5178,8 @@ packages: cpu: [x64] os: [win32] - '@immich/sdk@2.7.5': - resolution: {integrity: sha512-gCTpFAWlFF+1Sx0jbqPVmwN+0JD+eZEHzhf3q+BbxuQF+ORpNqT0D453mOyonogvy17nJF1jGA2PtT6vOQiSnA==} + '@immich/sdk@3.0.1': + resolution: {integrity: sha512-+iY6cd5Fc0cDIQOpXCSJHJwByaNeFj5MXzFT0xqRlXLE1G2fizoRt48BQ5eVoI64swDtwevnFIlCLYgydF/qqw==} '@inquirer/ansi@1.0.2': resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} @@ -5442,88 +5578,88 @@ packages: '@libsql/core@0.14.0': resolution: {integrity: sha512-nhbuXf7GP3PSZgdCY2Ecj8vz187ptHlZQ0VRc751oB2C1W8jQUXKKklvt7t1LJiUTQBVJuadF628eUk+3cRi4Q==} - '@mantine/charts@9.3.2': - resolution: {integrity: sha512-G3cFLLxmmRr0ij5fOHeO/jqR/U+DnMNZy0S7GEvs4lIzDRl871TDb1BogZyeHUGKuLnsLBYpTA719IcmswYGMw==} + '@mantine/charts@9.4.1': + resolution: {integrity: sha512-sGP1Org+nkkyJ81o6uBoUiAffVsy8Uemu+uwVVNaCVavDrRBGj3I4wCfSNLQdrwr+aoFbDLAyWaM0ON9bZWCbg==} peerDependencies: - '@mantine/core': 9.3.2 - '@mantine/hooks': 9.3.2 + '@mantine/core': 9.4.1 + '@mantine/hooks': 9.4.1 react: ^19.2.0 react-dom: ^19.2.0 recharts: '>=3.2.1' - '@mantine/colors-generator@9.3.2': - resolution: {integrity: sha512-q2sqR2vCwdkDD/OuTcmfvkg66Wc6avepYB+073qh75YP3+EPa60Ng/h3Vfgmt+G2iTsqURJgCAbCLaZZr/9g4Q==} + '@mantine/colors-generator@9.4.1': + resolution: {integrity: sha512-HEJAJACPfS5Y2XRf8ILOUpH7/nag7YN8v3SxGo+vTCyvWnNNqNM19V+weohrHSLnM+TLgHehFzHdG8CzmJkiXQ==} peerDependencies: chroma-js: '>=2.4.2' - '@mantine/core@9.3.2': - resolution: {integrity: sha512-Upy/Z9Sj2eW2dGrFgUy/2kISVsxMTBYTDfP2TFdsIA3PdPSBkdqfSOPX/ug3d3F3a4bnwQSqcO6+aPEpBIh8dg==} + '@mantine/core@9.4.1': + resolution: {integrity: sha512-lZWEICrum4+vwKxzh/mk4RB1N6BqZ0Cshdl6lhm7OYLiQzmOaxKtOgyaWz+vr65G8mqTXjalZalB+wmMVBYK2Q==} peerDependencies: - '@mantine/hooks': 9.3.2 + '@mantine/hooks': 9.4.1 react: ^19.2.0 react-dom: ^19.2.0 - '@mantine/dates@9.3.2': - resolution: {integrity: sha512-MzHrXGoOb3rCnHBlsI/BPAjC8K5tZ4f8pzg66tsTsupVCQBaBpHSVatQwNRJ6K2JC9pyCyTa9BL803C8AiWycA==} + '@mantine/dates@9.4.1': + resolution: {integrity: sha512-DoQDvQMAlIw6ohwhUwYuTYQevG9y5AQmYWkhOHUuavABFEZmtBglkwziBoATvxT09I/6I56UMPhproXcS6XUfg==} peerDependencies: - '@mantine/core': 9.3.2 - '@mantine/hooks': 9.3.2 + '@mantine/core': 9.4.1 + '@mantine/hooks': 9.4.1 dayjs: '>=1.0.0' react: ^19.2.0 react-dom: ^19.2.0 - '@mantine/dropzone@9.3.2': - resolution: {integrity: sha512-wx/6wSVLVW+OfPU9FgkMkfnaUd7roPZG1zOlzE6EIRYIqsHciY3OAlrhQDpYnTkowsseJpXDg4JG788l5kNDUQ==} + '@mantine/dropzone@9.4.1': + resolution: {integrity: sha512-QmtwDsViRmIdiwpc3d1loV3ryb8L6O4y3aiuMnAcLxL31a9hUPlHUZAs6KSuA+XbziZDG4BjxRVYjB70PxwORg==} peerDependencies: - '@mantine/core': 9.3.2 - '@mantine/hooks': 9.3.2 + '@mantine/core': 9.4.1 + '@mantine/hooks': 9.4.1 react: ^19.2.0 react-dom: ^19.2.0 - '@mantine/form@9.3.2': - resolution: {integrity: sha512-jWEXcx8hhddOsny+q/lFtVToNS3VBJYICrkfFMWJsN+WAZamnPItWTPLqulTYVp1T8uma9MsEZ1LX4kqWUFANA==} + '@mantine/form@9.4.1': + resolution: {integrity: sha512-e4tqC/D8ii7maPySWIWY306MNljPRprHPhu0/jaZrX7sRyJy+C8V9aU+ZFKYliM1CeiH1UX88+q4hWFUWVEdQw==} peerDependencies: react: ^19.2.0 - '@mantine/hooks@9.3.2': - resolution: {integrity: sha512-jOjpUe0x1A/k3XUiu2/aaSCasXRI5ZKZOucm3ypwsvm9F2u8C1xzwBvagrzlbNZwJRF5vNYIJtCaT7NunPyc0A==} + '@mantine/hooks@9.4.1': + resolution: {integrity: sha512-eTI8wmzPx3r98zgKIEuvukmoGTHBhmtI6+9E6o2DbTmEU2eM1bCdjE2vFdf0op2AlRO0KEYEcZhNQi4T/SJk0A==} peerDependencies: react: ^19.2.0 - '@mantine/modals@9.3.2': - resolution: {integrity: sha512-XorW2PcYU+DQQVZmY80OlwI/bgkBPzf9MscjEZep8Z3aowrlNpnJXFbu2RoPCPMwROU2nX4gt0HJP2OiuGr28A==} + '@mantine/modals@9.4.1': + resolution: {integrity: sha512-z5G3Biz7/5Ybz5Z8u5Xyvo9UBmFhAX/YXMf6U96ejvtAB11xVnCo8m37R5DJyzeFEqB4D1tvUgWiVVPBb+yadw==} peerDependencies: - '@mantine/core': 9.3.2 - '@mantine/hooks': 9.3.2 + '@mantine/core': 9.4.1 + '@mantine/hooks': 9.4.1 react: ^19.2.0 react-dom: ^19.2.0 - '@mantine/notifications@9.3.2': - resolution: {integrity: sha512-mnda8hE/DizPi5Bbiwd2M2jXxfSZSUeayvnLYMbFweGQKXM+QRmX1ootyDRVqWew8paltewTNp84cWLjj/6fKw==} + '@mantine/notifications@9.4.1': + resolution: {integrity: sha512-Sm1g8E8RkFmesxpNz9zFvOjkTVxOa7dJJjMGoHNIXH+9/wO0s9kInny4mVnxTbLJJsD6nTqIQlk1v8tq2C4Ftw==} peerDependencies: - '@mantine/core': 9.3.2 - '@mantine/hooks': 9.3.2 + '@mantine/core': 9.4.1 + '@mantine/hooks': 9.4.1 react: ^19.2.0 react-dom: ^19.2.0 - '@mantine/spotlight@9.3.2': - resolution: {integrity: sha512-JAJhW51qvYNiPTEnX9ewZX521gKDsVsmaecuuR2iNyk0NbrOPB5SWF9ZZrPSMwFVtqnqYHziq5BLukzlf+kG/Q==} + '@mantine/spotlight@9.4.1': + resolution: {integrity: sha512-gJ+7DdnGULeobtGXVGbVgvJSG3TeecxLXNxzpZo+ohl1OetPftCBGJOUrX2tYOOC6nJE1eHSIr/kpB31YIRjhQ==} peerDependencies: - '@mantine/core': 9.3.2 - '@mantine/hooks': 9.3.2 + '@mantine/core': 9.4.1 + '@mantine/hooks': 9.4.1 react: ^19.2.0 react-dom: ^19.2.0 - '@mantine/store@9.3.2': - resolution: {integrity: sha512-uNmvjkwfUHZJ5bvzQroLFvubuUAHjBM0rh69RBRsUCG5FR5K4f8y4SX8+lch0wS6UJuuvodsSIK8H5jvZuFSgQ==} + '@mantine/store@9.4.1': + resolution: {integrity: sha512-/DCZq0aqqIdm03J0RJ7pbzoau2cwY8ndSOjskHOKHhTeWMnOBdCKXA3oMO5aIDldDqJ9T7Io4Boh4Pt461zNQw==} peerDependencies: react: ^19.2.0 - '@mantine/tiptap@9.3.2': - resolution: {integrity: sha512-X344wqt3eusMLPANWuNSnKoFjTDlCEOUpYq6hPWU6uBvxEHJGHuJFXjtm/jg/D6aNEvWr+P+m1+ElE7eLT/G0A==} + '@mantine/tiptap@9.4.1': + resolution: {integrity: sha512-0qN6SFh9BuwXNOHUf4ygx8LPvWcgU1zUULY7IJ9zKiVHfa1K6b0BRa8LpLVKtVv7K5NLyj2uHJwv+mXB+ypOJw==} peerDependencies: - '@mantine/core': 9.3.2 - '@mantine/hooks': 9.3.2 + '@mantine/core': 9.4.1 + '@mantine/hooks': 9.4.1 '@tiptap/extension-link': '>=3.3.0' '@tiptap/react': '>=3.3.0' react: ^19.2.0 @@ -5589,63 +5725,63 @@ packages: '@napi-rs/wasm-runtime@1.0.7': resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} - '@napi-rs/wasm-runtime@1.1.4': - resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@next/env@16.2.9': - resolution: {integrity: sha512-ki5VxxXfzD/9TDe13wyeTKIjQTAwBVpnr8KhRDUr8ltMUq1/NBpWNT5tiPoxiGl+PHM4X2ahSOiPk6iAimIzPg==} + '@next/env@16.2.10': + resolution: {integrity: sha512-zLPxg9M0MEHmygpj5OuxjQ+vHMiy/K7cSp74G8ecYolmgUWw0RwN02tF56npup/+qaI8JB97hQgS/r2Hb6QwVA==} - '@next/swc-darwin-arm64@16.2.9': - resolution: {integrity: sha512-HkfxNYUCmcct0Xsqib5KxqMSHV4AHJq857BNRchyBDs4YS19aHzVfn1kDuBYKqLLQBjXgnkIsjV2Kd4d2wzYhw==} + '@next/swc-darwin-arm64@16.2.10': + resolution: {integrity: sha512-v9IdJCa0H0mbo+8z5zwUpOk1Vj7RjkcI5uNYf5Ws1y6szf/p3Mzl9hLaST8SCt6L9h8NGnruZcd2+o0NTNwDhA==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.2.9': - resolution: {integrity: sha512-7IAtK4MeybpqRV9GRABWEhJ62mOS+rzWOzOTFie4cSEtm12xsoOMJRcECoZx3FHPzFAqN/IJtHqWAFOLfl152w==} + '@next/swc-darwin-x64@16.2.10': + resolution: {integrity: sha512-17IS0jJRViROGmA9uGdNR8VPJpfbnaVG7E9qhso5jDLkmyd0lSDORWxbcKINzcFqzZqGwGtMSnrFRxBpuUYjLQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.2.9': - resolution: {integrity: sha512-hBD75iWpUtkL9SmQmcRhmLomn9jgkPzCEkbOcLgHymPEKzv+6ONy13RRiIEz/iEObjkS2Jlb5gYS2XGoS3X4rw==} + '@next/swc-linux-arm64-gnu@16.2.10': + resolution: {integrity: sha512-GRQRsRtuciNJvB54AvvuQTiq0oZtFwa1owQqtZD8wwnGpM2L39MV22kpI72YSXLKIyY40LC66EiLFv4PiicXxg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@16.2.9': - resolution: {integrity: sha512-qZTI3pf9SGc/obr8NkQAekBxmp1QK+kVm+VAf3BALLfFAj+1kUhkTxmrWpVos9R/UYIA8AWX2p6cGI5WdwzVUA==} + '@next/swc-linux-arm64-musl@16.2.10': + resolution: {integrity: sha512-zkN9MQYS7UQBro+FnISUq1itaQjXI9xqISzuQ+2bc921NcJ1x4yPCqrn77tVN6/dOOXaaWVX3k6/bR07pPwK+A==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@16.2.9': - resolution: {integrity: sha512-xm0HfRNX+UkH4R3c18ynswjj5o5uEj/7iI9p9omdtTSIsRCzQqkGMA+10nzJ4EHnYC3as65IMhbbl5fWRUWHYg==} + '@next/swc-linux-x64-gnu@16.2.10': + resolution: {integrity: sha512-iCVJnwvrPYECvA6WM/7+oo+OiTvedIKLxtCLAZP4xZR3nXa1zmzZyLPbYCmWvpd4CvMYF1EMTafd0ii3DygLvA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@16.2.9': - resolution: {integrity: sha512-QumimHkGEG6vM3PfEDWKyKen03NcqLOkeKB1EfcPe7VxzmEiCa4jNnMyBn/US5zcd/VE1CI+O8Ovb3lfjVHfGw==} + '@next/swc-linux-x64-musl@16.2.10': + resolution: {integrity: sha512-ov2g4H0dHY9bPoOU83m91hWT7Iq5qy13bUnyyshLU3HGR1Ownn0X9QpmDPc5iIUaahTp7f7LeGAhV4DSFtackw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@16.2.9': - resolution: {integrity: sha512-hzQpKZvw8rAwI6A2uQh6SacCSvNAXaIkPNsWwzqqfRiIMiXMfH936skDhz1OO6KpvdKkJrgHHtqQOq5PIXOvdQ==} + '@next/swc-win32-arm64-msvc@16.2.10': + resolution: {integrity: sha512-DwAnhLX76HQiFFQNgWlcK+JzlnD1rZ+UK/WY0ZMI/deXpvgnesjNYrqcfo1JzBuz4Kf7o3brIBL0glI1junatA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.2.9': - resolution: {integrity: sha512-qr2VL3Ce5QrwgO2yh1ujSBawrimjVKX8FGF/cOynmdYKJY0BdHpGVNIRK1tqONB10Vkm25Ub1BD2bkjWs4+96w==} + '@next/swc-win32-x64-msvc@16.2.10': + resolution: {integrity: sha512-0JXq3b85Jk9Jg4ntLUbXSPvoDw3gpZou7twuKdoFG2jOw635v7+IiXfTaa0TxVMyx78pUjnrVYwLgjKfX4e6/A==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -5715,6 +5851,9 @@ packages: resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} engines: {node: '>= 20.19.0'} + '@nodable/entities@2.1.0': + resolution: {integrity: sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==} + '@nodable/entities@2.2.0': resolution: {integrity: sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==} @@ -5860,249 +5999,249 @@ packages: resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==} engines: {node: '>=14'} - '@oxc-project/types@0.133.0': - resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + '@oxc-project/types@0.138.0': + resolution: {integrity: sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==} - '@oxfmt/binding-android-arm-eabi@0.55.0': - resolution: {integrity: sha512-+rFDOqQe5LOWgxrAJaZgLRudr6GQm0wGI6gtu7vVkrdLGjNMUSGbAlaCr8j7F2H2Er97vYQCU8WDb30onqMM1g==} + '@oxfmt/binding-android-arm-eabi@0.57.0': + resolution: {integrity: sha512-qVBsEO+KugOsCmUHcO8iqNnqc65p7PCKpCs8M66mPZ+Ri+CWbcpoQOEJBg2OTu03+0qu++NK1jj6IzvQVs0Sig==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxfmt/binding-android-arm64@0.55.0': - resolution: {integrity: sha512-ctulLq8s3x8Zmvw6+iccB09TIKERAklRSmbJ10gk8mlAn05qZxoyo52dj3Hi9IJcmDSwF54fQaTVh2CbL6PInw==} + '@oxfmt/binding-android-arm64@0.57.0': + resolution: {integrity: sha512-mp6PibWbao3aizijcheOeHQaYEhcUAt8pwLniYbtLfHxL/psFF0BykAwCj+s3c6qIpa8yN8keZICWrqtZ70w8g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxfmt/binding-darwin-arm64@0.55.0': - resolution: {integrity: sha512-xDQczLH9pw/RBk1h/GH0qcGMm8hQtmtVHBNLSH3lk1gEIR09hZ4L+mJQl4VqiVAvPK9VG9PYrWWuSQLt7xTbiA==} + '@oxfmt/binding-darwin-arm64@0.57.0': + resolution: {integrity: sha512-T+0stuCBqmUVY+aMIvrgXhzGhHO3sD5tNiiEcYqgSdPsnukskQqn2u5qOVD0sv1l7RLdFS5Z/f5Wi9Ktyjr3Eg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxfmt/binding-darwin-x64@0.55.0': - resolution: {integrity: sha512-JaNoFCkF2CJdGgpPSMbuO9HVyXyoNGIhMHPvp6NYAjeVKw9XEYc0HcUWJLPQa3Q69WV5wMa9m5jPMJPtbLtcRg==} + '@oxfmt/binding-darwin-x64@0.57.0': + resolution: {integrity: sha512-O+3JbqWs/mCI2oi4xfhRO2IVPFJNDDEBV8Odo+ZpmsUOeKJfjXoNH7nDmBEQcDgK7NfjDIyE7kRgYSZcTLDO0A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxfmt/binding-freebsd-x64@0.55.0': - resolution: {integrity: sha512-DNbszhpg6S2MIzax5azdHFTTBIVkR5xr8yyRZuA4yoDAwOkzIp3tmldgKZM2+VlT+hJIG0xUksA+elISzMEAfA==} + '@oxfmt/binding-freebsd-x64@0.57.0': + resolution: {integrity: sha512-pxwhxVC+JkLX9twOQ/8C/vbuOQcMZyKIDmiRDZfO7yITuVcIdZCiLRqqf4QOxb2+8FWrRXzQpm+1DBKcMpHSSQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxfmt/binding-linux-arm-gnueabihf@0.55.0': - resolution: {integrity: sha512-2snoaoRfFFyGnbOcKUK36rREBYxe/Xgz3uHbiA5zbCB/s6R4DQj4mHqYAaWWhgizCUSDxV8cE9zAZ0XleNpKGw==} + '@oxfmt/binding-linux-arm-gnueabihf@0.57.0': + resolution: {integrity: sha512-pxBU4zH2imB/MDBfth2rOMeVxXUMjRQLCazagwLARIFH3hVlxZJBlM4nSnHXaIHJK4/qezoFCIORN6AY8Mra4A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm-musleabihf@0.55.0': - resolution: {integrity: sha512-q1aktHF/WRpSK81BX1dE/9vWrS2jGw1Nax2kb4DBLGAewubCLcoNyp4Zl/NSMgbv3vUS46Z33wIQkBVYOP3PYg==} + '@oxfmt/binding-linux-arm-musleabihf@0.57.0': + resolution: {integrity: sha512-JAprOzt8tycYou36ZgEw14DlRHTiN8qdtKANdV3VZIRIvTI/lh/cX13c9pJ/EnDk2GT3FASH7KvCgQ2AufAifQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm64-gnu@0.55.0': - resolution: {integrity: sha512-VD0y36aENezl/3tsclA/4G53Cc7iV+7Uoh7gz4yvcOTaEYBtJpQsE6PKDGTtUtOvGS4kv51ybfXY/nWZejO5IA==} + '@oxfmt/binding-linux-arm64-gnu@0.57.0': + resolution: {integrity: sha512-ajtjaxSaj9xl4BW7REt+Cef/ttzbAq00Bq4z7JUDZEfgFXdwSjH8K9bF+IcIJzZB9lKqMfQ4eHuSFOvvlvtqOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-arm64-musl@0.55.0': - resolution: {integrity: sha512-r8xlKJFcsRmn0H5jZrdORae6RX9jDBrZVvOoxF+bCQtampQJClv80aZEHsv+NsLsp2KCE5ql79O7DpPVzYWpXA==} + '@oxfmt/binding-linux-arm64-musl@0.57.0': + resolution: {integrity: sha512-p4Y/+RYk9Bk5WO+zHSUXAClRmZ2fbJCejMuCAsU2HhyME4jqf6Ftt/mJYEwIah1wGCBDYOB7wEGV1x5bCEZ6hA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-ppc64-gnu@0.55.0': - resolution: {integrity: sha512-GRKv/HXHcwIVld/WU61rF0g0R16hl5EJ+ScKdpjevT57lnLnagj/U2YUbXf2mT+2Pg1uCzWC+mvGicPV3CDdLQ==} + '@oxfmt/binding-linux-ppc64-gnu@0.57.0': + resolution: {integrity: sha512-By6tRALAZsno0F4zedmtG+wdMvJiJmJoXM4d3+A9zHE4HRXLqXITwRH8mgrlcXc5yJM2g2W3riRPwTYdgemZLQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-gnu@0.55.0': - resolution: {integrity: sha512-rdv57enTiPtpSYRMKfAiEbQb0Puw5t9N7isVinDoo5qeLDScro2gznmZqSgSWbVZRzLisTeCTW8Qwgw0bOHv3A==} + '@oxfmt/binding-linux-riscv64-gnu@0.57.0': + resolution: {integrity: sha512-skYeG+RgvyzspqVEBsEprL90OYYZfoVNqB3HcCNR6QDJyXKOzfDRT3zncnHmUaFluIlBHuY23mU1b5WGgR98hA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-musl@0.55.0': - resolution: {integrity: sha512-7v1nNrlD43VY6+sYQ6efYyb3lE6QY182304PD/768ZxTjOmFd/3dQa3u/nGBUAXYdGSWOQc5N3PnS0QzUXyEIA==} + '@oxfmt/binding-linux-riscv64-musl@0.57.0': + resolution: {integrity: sha512-FFgACrZOXAXUh5KQh2mt1CDOVOZmn+QzHP71wM9QobNwyQvoFfyAeefVUltW83g3sm7LTiH3yfFqLLVUpA5ZFQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-s390x-gnu@0.55.0': - resolution: {integrity: sha512-f4lJLUSPOgScjFl9LiflKCTocyNRwE25JmTMbN4XQdDjoZzEHjqf3wA3VESF1/csg7i8m7+EQLbrZyYDqe10UQ==} + '@oxfmt/binding-linux-s390x-gnu@0.57.0': + resolution: {integrity: sha512-Nm/BAOfQeFiiKd502mZn/GAVKJwtd0RdCg17G3Wz/WSOIQmDi3+7/SZH4BHn1Ye5KvTVH3ua8WvfwLLycNIuvA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-gnu@0.55.0': - resolution: {integrity: sha512-MihqiPziJNoWy4MqNSV+jVA1g+07iQDjZiR0vaCaDoPgFEiJpCMsxamktzLV07cEeQsSJ04vQaU4CzCQwIvtDA==} + '@oxfmt/binding-linux-x64-gnu@0.57.0': + resolution: {integrity: sha512-BiSy5Ku3mQqyxS6YIqAJgd403wEUWvI7kerfzPxc2l/txZVmZM0pSj7oDM+4bGBExowxOi7o73jEam1W0EDTZg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-musl@0.55.0': - resolution: {integrity: sha512-Yqghym7KYAVjP9MmSrNZiDeerMuoejNjo0r3ox5H3GDKk8eAfl8VyJm9i+pWCLDCTnAbcTUMMN2ZKjUYXH1v3g==} + '@oxfmt/binding-linux-x64-musl@0.57.0': + resolution: {integrity: sha512-BCRkJiotz5s9afLYD2LuMvzAoDYx9H17E/YbDyu4xK7l4zHDPeny9ErSXL//i/nJyaOwRk08x4b8cgJC00+JDg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxfmt/binding-openharmony-arm64@0.55.0': - resolution: {integrity: sha512-s5SDvVVSbyQl1V5UU3Yl12M+XLUQ3rl5SglNqgAA2K4PXUtQhyNSS00wivONPEnNo5W01rCou8WkDNyvI/RGHg==} + '@oxfmt/binding-openharmony-arm64@0.57.0': + resolution: {integrity: sha512-4Oaxe1qrGgXfpCJ1C/ERJ2iCtV2rN1R79ga9fsfyVHfSQRu/hVW780u2KDqZWFZ/iGTHODJji0JemxqFZ63eIQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxfmt/binding-win32-arm64-msvc@0.55.0': - resolution: {integrity: sha512-7p9FB5R32tw2KyyNX3wpQrR2WHwEHvMEiBlGXxeTCaRMCVNx3UtFMAUbaQ/pRNWIrEUZmYhJ6tcUH52uPTRYjQ==} + '@oxfmt/binding-win32-arm64-msvc@0.57.0': + resolution: {integrity: sha512-MYLAsDnhdNsSGheLYhWgbk0vfIrlS84iQYun/y21fX6u0jj8iBtYtbpZMdiqYeuf8U12eVPUjVY2xE2NrCfJ0g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxfmt/binding-win32-ia32-msvc@0.55.0': - resolution: {integrity: sha512-ZYqj3fDnOT1IaVGMP5kpmkQl4F3tQIm2ZyAxvqkJYmI0xgWWak4ss4XYwv3VDfM+TWXeC9K4uQ/wW5jm/5XABA==} + '@oxfmt/binding-win32-ia32-msvc@0.57.0': + resolution: {integrity: sha512-PBwdzZALJY/jcCx2E6is0yu+cuVXeySTDmwuseD+9j0mHqlRNxwlKgsyRTBed/woPeqfVfuXfWjoq4Cx2Zt3Eg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxfmt/binding-win32-x64-msvc@0.55.0': - resolution: {integrity: sha512-eEYT5tivGnGbPHuOHuQpi6CGLObhh0re/5jcNQHihD2GRYkTM85dyi5a19zjP8Q00t1uqAx+/QGLUGdHeqzWyg==} + '@oxfmt/binding-win32-x64-msvc@0.57.0': + resolution: {integrity: sha512-bQJdH9i4RRfw55jm7+8/xS7GzHLLTbHx4huhrrDxQJaJtbSDbsyOnODvP1ftT7EG0KFKAYO2S+q6AcioXODx8w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@oxlint/binding-android-arm-eabi@1.70.0': - resolution: {integrity: sha512-zFh0P4cswmRvw6nkyb89dr18rRanuaCPAsEXsFDoQY8WdaquI8Pt4NWFjaMJg6L23cy5NeN8J9cBnREbWzZhaw==} + '@oxlint/binding-android-arm-eabi@1.72.0': + resolution: {integrity: sha512-zhCmvn+1Mj3UchAc/90i99S0t7jJUsHmFVSPg4UWrjO8b8eaSGwscgO6QAUtvHBstkjQwBttQNswEnAF1mIQdA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm64@1.70.0': - resolution: {integrity: sha512-qI8o4HZjeGiBrWv+pJv4lH0Yi2Gl/JSp/EumBUApezJprIKa5PS4nU0lQsQngtky8k+SplQIOjv6hwu0SSxeyg==} + '@oxlint/binding-android-arm64@1.72.0': + resolution: {integrity: sha512-mtH+aY/ozv1eZoCUC2owjFAtyNBKHpJHygKeEu9zXXnQGW1Q2/qOpvx+I+Lf23+TvTz66F4iiXUbl2cGvoLPCQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.70.0': - resolution: {integrity: sha512-8KjgVVHI5F9nVwHCRwwA78Ty7zNKP4Wd9OeN5PSv3iu/F/u1RVXoOCgLhWqust6HmwQG6xc8c+RCyaWENy24+w==} + '@oxlint/binding-darwin-arm64@1.72.0': + resolution: {integrity: sha512-EvnajNPDtfknB3ZieeOOyDTwJn9QXDiwfnF4ZDQqART6RG6hjY4WigQcZdGoK2dkB3e1vrmEzN9aYbQCUkh/gQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-x64@1.70.0': - resolution: {integrity: sha512-WVydssv5PSUBXFJTdNBWlmGkbNmvPGaFt/2SUT/EZRB6bq6bEOHmMlbnupZD5jmlEvi9+mZJHi8TCw15lyfSfQ==} + '@oxlint/binding-darwin-x64@1.72.0': + resolution: {integrity: sha512-ZkCdEa/G80A7vEHfeCDz/+L3m33DE73v32mDKhgOIgz8Uwf0DFcK7+uu6qC+7LEhmz5fpOe1osWKyjSNMydFIQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.70.0': - resolution: {integrity: sha512-hJucmUf8OlinHNb1R7fI4Fw6WsAstOz7i8nmkWQfiHoZXtbufNm+MxiDTIMk1ggh2Ro4vLzgQ+bKvRY54MZoRA==} + '@oxlint/binding-freebsd-x64@1.72.0': + resolution: {integrity: sha512-NroXv2vh+sxVY1uya/rM5pjhx1hm8BzlYpx9q67QP0Xhw5MH2bf5GJylpvLEC+781p1Xli/317EoV9AlGwViag==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-linux-arm-gnueabihf@1.70.0': - resolution: {integrity: sha512-1BnS7wbCYDSXwWzJJ+mc3NURoha6m6m6RT5c6vgAY3oz7C3OVXP+S0awo2mRq97arrJkVvO3qRQfyAHL+76xtQ==} + '@oxlint/binding-linux-arm-gnueabihf@1.72.0': + resolution: {integrity: sha512-0NDywYgfj279Ou/BcQuCYSj7NJwBfmWn5qc5uGO/Ny7fUWmXyIpvawqX/8acQlWG6IXelJsJhj+JAy6sjsKj0A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.70.0': - resolution: {integrity: sha512-yKy/UdbR55+M2yEcuiV5DCNC/gdQAjr/GioUy50QwBzSrKm8ueWADqyRLS9Xk+qjNeCYGg6A8FvUBds56ttfqg==} + '@oxlint/binding-linux-arm-musleabihf@1.72.0': + resolution: {integrity: sha512-4vpXB06h65Ezsy4hRyrGjGrfa1SkVPii09yaajiYhmVpgsFiLD+KNxIx/BNAY+XiO+i1yqp9HHdwqM8VTqa5XQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.70.0': - resolution: {integrity: sha512-0A5XJ4alvmqFUFP/4oYSyaO+qLto/HrKEWTSaegiVl+HOufFngK2BjYw9x4RbwBt/du5QG6l5q1zeWiJYYG5yg==} + '@oxlint/binding-linux-arm64-gnu@1.72.0': + resolution: {integrity: sha512-immaN4g2ZGFiOkKrvRX9LvzZdd2GkQM5wR+UyzYyUuyhUTXGQ4HKUJH18xp4G8OfhCVaVAJfKZxwE1r8+4hhaQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-arm64-musl@1.70.0': - resolution: {integrity: sha512-JiylyurlB0CLSedNtx1gzv3FvfWPF1h/2Y3BJszPLNt5XQFlBsH5ke0Jle3iJb3uqu5m2e7A/DwzpuCAHdiU+A==} + '@oxlint/binding-linux-arm64-musl@1.72.0': + resolution: {integrity: sha512-JGHS9Mnr7iWyyLDxgCv1MhzVpAckgptg00F2gnxt/GD7lQ2SW1BRcxHqhSTaSdDpjWRrBkBxMMh4+Hn3aVtExg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxlint/binding-linux-ppc64-gnu@1.70.0': - resolution: {integrity: sha512-J8VPG7I3/HmgaU4u8pNU2kFx2+0U+vPLS1dXFxXOaR/2TQ0f8AC7DRz0SRGRI1bfphnX2hVYTTtLuhL4nYKL+Q==} + '@oxlint/binding-linux-ppc64-gnu@1.72.0': + resolution: {integrity: sha512-AOYgBZqxNshrg83P9v0RYv+m8s10Cqkj4/PxXFDhcS3k7FqsIG5+CxErshZCIN7G8iy4Y+VGfAsuEdar8AcbBg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-gnu@1.70.0': - resolution: {integrity: sha512-N2+4lV2KLN+oXTIIIwmWDhwkrnvqf5oX7Hw0zPjk+RuIVgiBQSOlJWF7uQoFx2siEYX0ZQ5cfSbEAHm+J3t7Wg==} + '@oxlint/binding-linux-riscv64-gnu@1.72.0': + resolution: {integrity: sha512-QMybPS5ij3/vrKG67mqzHwW++91sYxK/PPUVi6SBtNCEzW4niS52fVBdXbQ6nou0wWbUPEpx8Sl/ZjtgE3clXA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-musl@1.70.0': - resolution: {integrity: sha512-1e2L7cFCvx9QDzq6NPP+0tABKb5z6nWHyddWTNKprEsjO9xNrAtPowuCGpjNXxkTdsMiZ4jc8YQ5SstZd4XK6g==} + '@oxlint/binding-linux-riscv64-musl@1.72.0': + resolution: {integrity: sha512-gOc3W7JV0PXRpIL7stUlLe3Wa9Gp0Kdlup87IT3gHDvPKck2xNgMIl/Gs2lldYY2lyXZDC4rWi3hmoLUobkgbQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxlint/binding-linux-s390x-gnu@1.70.0': - resolution: {integrity: sha512-Kwu/l/8GcYibCWA9m9N5pRXMIKVSsL/YbgpLzYkqDhWTiqdRfnNJ/+nqIKRKQiFbHWsdlHEhzMwruJK+qcEruA==} + '@oxlint/binding-linux-s390x-gnu@1.72.0': + resolution: {integrity: sha512-rpGxph+FjjHcYI5q6uxB3Az+tnfmEnDbSA8+PK9ZE/VzyUAkvBOMeuY7ZQMhu5mpZH7YQDsTdW6Cx4kV/msc6w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-gnu@1.70.0': - resolution: {integrity: sha512-tap04CsHYOl0nSAQJfPNIuBxqEPB2HnhQqwaOXLg1jnp2XfRo8Fa814dA4QC4zpvTWXCjAAaCY1W5LOORkEQuQ==} + '@oxlint/binding-linux-x64-gnu@1.72.0': + resolution: {integrity: sha512-WND+uhf/Ko13SLqQMWQUgsZuLvYYEvL0ZKgg0tgGYfLqxG7l8Ju123fHDMJyYSDl5E3bUbpFUuii/OvMreFQzw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-musl@1.70.0': - resolution: {integrity: sha512-hzJa/WgvtJpbBD9rgfy0qe+MjbxOXNUT0bfR1S6EQQzfTtBFA9xg5q8KSwRrQ2QfSS+TaP4j+4mVPQrfNc6UNg==} + '@oxlint/binding-linux-x64-musl@1.72.0': + resolution: {integrity: sha512-SrpbrUL70nG9vh6zP4/oKHWgLuHquwsr7MW9XOn0olBVgh10Uqr8qscKhQoBGEn6olK/IUpn5GSKcdQ5AjUhGA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxlint/binding-openharmony-arm64@1.70.0': - resolution: {integrity: sha512-xbsaNSNzVSnaJACCUYr1HQMyY/Q/Q1LkePmHG3UvZPvGCYGNxrsZp9OmtA6ick8xH47ltRRbRrPCM1YXYcyC+A==} + '@oxlint/binding-openharmony-arm64@1.72.0': + resolution: {integrity: sha512-qkrsEn6NmgFKr7U/QnezQMb+q/vzAy0Dd9Y95gQGQTyjzDLN+HRZMuM5u70iyH4nBLCfKBzhjMsYCehKay2jyg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.70.0': - resolution: {integrity: sha512-icAEsUI7JbW1TMRdEXV83mVAInhRVQYuuAlPpxdGwJ95chNdnCzjloRW8GglT0WvzOEZSio6fnYSk2DJ2Hv7LQ==} + '@oxlint/binding-win32-arm64-msvc@1.72.0': + resolution: {integrity: sha512-LWR6ZlFZph+KPjXv8opgZsXRDCdrdQe8VL8Cg9zxCoBS73h6znzZpydVgmdnwj8mB9AuSM5jxEgDJDpQkjboeg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.70.0': - resolution: {integrity: sha512-FHMSWbVsPVs/f+Jcl04ws4JJ2wUnauyTzlpxWRG/lSO/8GpX08Fo2gQZqdA6CrRFI+zvkxl+N/KwJGWfUwYVZA==} + '@oxlint/binding-win32-ia32-msvc@1.72.0': + resolution: {integrity: sha512-yt6HEh7IsHvtjRWtmeZRX134eaXKHq5Gnqlf1xBJdJl1JtdoRUEJw3nAxpZoUDS860cX/foKbztO441anVBtVQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.70.0': - resolution: {integrity: sha512-ptOlKwCz7n4AKs5VweMqG6DAg677FmKOK+vBkkL9DMNgFATIQ+upqUYBTOEwRQyRAx1ncGlPlXleV2hIcm3z4g==} + '@oxlint/binding-win32-x64-msvc@1.72.0': + resolution: {integrity: sha512-b2eKFD2hX7tIwmo/cyH6TDq8vzWRZ2qNHrzoGntUTmq0h3zQh/uX3eTSHCwI8OB/ADQfJCRelLItK8BsxuucDA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -6245,8 +6384,8 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@playwright/test@1.61.0': - resolution: {integrity: sha512-cKA5B6lpFEMyMGjxF54QihfYpB4FkEGH+qZhtArDEG+wezQAJY8Pq6C7T1SjWz+FFzt3TbyoXBQYk/0292TdJA==} + '@playwright/test@1.61.1': + resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==} engines: {node: '>=18'} hasBin: true @@ -6265,11 +6404,11 @@ packages: '@polka/url@1.0.0-next.25': resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==} - '@posthog/core@1.35.3': - resolution: {integrity: sha512-EsGPbSLl39Jgo2KZ+kI9UAxFnh5nddaN5bNm2rXvUwF+vGmam9eN1EXeNbxhRU7ulEeIiGdm7XjoU7pzavkgIQ==} + '@posthog/core@1.39.6': + resolution: {integrity: sha512-o6ajIwN5zXoNP0D4H/QPmOyibNTUkSyOR6ya7AG5U2ywXx4awo72L2KnCoiZPQM5x/bXv6jPBdimH8M18Ax0aw==} - '@posthog/types@1.390.2': - resolution: {integrity: sha512-WcfKz2GNn2vfDX8vXmJYbKxegPxVWHuDQ/pHdAn0HoZDXDFnEp/+x3qBQA+fEvtbPjjtjgAt2wIgJMlM7asx7g==} + '@posthog/types@1.392.0': + resolution: {integrity: sha512-nctNujXL3FC1v99FktaTMSugSD9ZOZekEpahUSafkU2TSvW+XGKNkQZbokuJtiWvPBK208dwMJva8UfBkChqpw==} '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -6379,97 +6518,97 @@ packages: '@codemirror/state': ^6.0.0 '@codemirror/view': ^6.0.0 - '@rolldown/binding-android-arm64@1.0.3': - resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + '@rolldown/binding-android-arm64@1.1.4': + resolution: {integrity: sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.3': - resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + '@rolldown/binding-darwin-arm64@1.1.4': + resolution: {integrity: sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.3': - resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + '@rolldown/binding-darwin-x64@1.1.4': + resolution: {integrity: sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.3': - resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + '@rolldown/binding-freebsd-x64@1.1.4': + resolution: {integrity: sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': - resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + '@rolldown/binding-linux-arm-gnueabihf@1.1.4': + resolution: {integrity: sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.3': - resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + '@rolldown/binding-linux-arm64-gnu@1.1.4': + resolution: {integrity: sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.3': - resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + '@rolldown/binding-linux-arm64-musl@1.1.4': + resolution: {integrity: sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.3': - resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + '@rolldown/binding-linux-ppc64-gnu@1.1.4': + resolution: {integrity: sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.3': - resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + '@rolldown/binding-linux-s390x-gnu@1.1.4': + resolution: {integrity: sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.3': - resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + '@rolldown/binding-linux-x64-gnu@1.1.4': + resolution: {integrity: sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.3': - resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + '@rolldown/binding-linux-x64-musl@1.1.4': + resolution: {integrity: sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.3': - resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + '@rolldown/binding-openharmony-arm64@1.1.4': + resolution: {integrity: sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.3': - resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + '@rolldown/binding-wasm32-wasi@1.1.4': + resolution: {integrity: sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.3': - resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + '@rolldown/binding-win32-arm64-msvc@1.1.4': + resolution: {integrity: sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.3': - resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + '@rolldown/binding-win32-x64-msvc@1.1.4': + resolution: {integrity: sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -6554,46 +6693,54 @@ packages: '@rvf/set-get@7.0.1': resolution: {integrity: sha512-GkTSn9K1GrTYoTUqlUs36k6nJnzjQaFBTTEIqUYmzBcsGsoJM8xG7EAx2WLHWAA4QzFjcwWUSHQ3vM3Fbw50Tg==} - '@scalar/agent-chat@0.12.11': - resolution: {integrity: sha512-JX1rFryU6kHHcMzzewr9o4Qe0st/27A8AFvka7v0OWsIoNJHcO9aA5gitedci6eZJNsNSBj6bXpH5VrCU1jAfA==} + '@scalar/agent-chat@0.12.16': + resolution: {integrity: sha512-YETdit7xhWpqdu4PMDM6AVQXID6GtJ1MSS0pmdyUFpR1xG02ABFWPsZHJ8+LQvFQXTC9ZOowtjgP8G9gTKA+KQ==} engines: {node: '>=22'} - '@scalar/api-client@3.10.4': - resolution: {integrity: sha512-TwmbM6hnzPs1vZUzucczNkPTzxTOtV+gu+TIq8/yEZBYF7GC6+6YerxPrGYjTgifp2UbIYf9GlSvmZv8RzSkIQ==} + '@scalar/api-client@3.13.1': + resolution: {integrity: sha512-+UWLBGY2dFFrbCYtEBHpjezZvSg/7eJ+CZs3oWo3Rr6JmVIJ/CKN31Ev5KAfce0w5Xmf7vbaWdExZheW+3ff7Q==} engines: {node: '>=22'} - '@scalar/api-reference-react@0.9.47': - resolution: {integrity: sha512-2vT7WCgU2AY2wrB+Xit09b76h5uQsQ/zP68LyTKSPYG8FIYeVsuGkrpIJGbtY/f57d3rQCqrbrZjLb30VzuWKw==} + '@scalar/api-reference-react@0.9.52': + resolution: {integrity: sha512-+BkephxdTM7VFH9U/joHjOHvKadfOB9sxTCrYnWoURevz6LkrvnAGKvoALbAZBS4jbvbHabfpu6uL3sqDnV1EQ==} engines: {node: '>=22'} peerDependencies: react: ^18.0.0 || ^19.0.0 - '@scalar/api-reference@1.60.0': - resolution: {integrity: sha512-godMRo3E/m6uZtm0zVfDa5xCg1IGD+eEt4uVWzR0v62mbm2O70ZWY9l+ondg8CrPQFsCdtWpWW+nYdzvp1tWTw==} + '@scalar/api-reference@1.62.3': + resolution: {integrity: sha512-0Q4NGSVK34tR/YMeQGxDD+v2NfH87/vDPxaBXxSyxfjg3lx/pmidt+YW2vcpxpoQdaPOVVwm8iuzcg0cxFjkCg==} engines: {node: '>=22'} - '@scalar/code-highlight@0.3.6': - resolution: {integrity: sha512-G/EtLF6a3IFGfIv5b7Uj5v/yXFY9+ktViVkfAkSoWJdHNBNk0S/Rb2OgRq24kp0D2TDXhLYF3E9OSHzquS7WqQ==} + '@scalar/asyncapi-upgrader@0.1.2': + resolution: {integrity: sha512-h6NUhsctrhucrbO2XHWbTXp9EWt7eL5vvlsooUEw2bB014KSPd41JrBQ6t0h/dFdmhwxdbBI7Hmg9eZa/mlCXA==} engines: {node: '>=22'} - '@scalar/components@0.27.2': - resolution: {integrity: sha512-cwIf042ehbD4dZWUIIlXZp0VWvaH7jznj/gpuLEqtuEDla5MrhTWYWj20aufpTpCeGexf1mqgouaWHUZ7lYByQ==} + '@scalar/blocks@0.1.2': + resolution: {integrity: sha512-vJQFlEckV4NTn5x95Q1Ff5HeeHwAqaX/8MLOX3O63F/XFXotIAp+MS8BQG2jnlVxzQn5omRSCNug2Y5J5ETZEw==} engines: {node: '>=22'} - '@scalar/helpers@0.8.2': - resolution: {integrity: sha512-qNbqUjSB3S4Gr4A0oANcm5G1Ip+EqBxICYKhe9YzmnaBpbmW6shxqpiivApTvvuDf+uIhR3uMwWyVQbYcGLsxA==} + '@scalar/code-highlight@0.4.0': + resolution: {integrity: sha512-ajUQ9oq5MwVHrXGze0SZVatgSXP/WbN4qgE0usYDFt1XuJ+O56TaBsCWb043r3Y9ZnHSnK9GHreVaLOxXsbf1A==} + engines: {node: '>=22'} + + '@scalar/components@0.27.4': + resolution: {integrity: sha512-1sbsMYGJcWiQ+5GouI1hHCn1VrytHPhzhHhCAZTXxi1Tw4SzsPSQuACARehXca+w2p7RK9ayGRYQ+K5bX18SaQ==} + engines: {node: '>=22'} + + '@scalar/helpers@0.9.0': + resolution: {integrity: sha512-M34CLRCttqC1bXthI/QSzQj0s5C6nrU2PFWf/vOT3RpycbiGDGQbqR+5RfFzpOIQvRqbHfNdcRbeiZBw+vCbkQ==} engines: {node: '>=22'} '@scalar/icons@0.7.3': resolution: {integrity: sha512-5uSUvumj6yJEAZT7/MKpgrkNl76waDXUpu0kUBBFJ83GhZirBIK+Z9SktShEvJT0+rk/j9Zaer3BHoEhYwAEBQ==} engines: {node: '>=22'} - '@scalar/json-magic@0.12.16': - resolution: {integrity: sha512-w8cDbZhHCzmIblWx92IVWoAXsbI4Fz3m++jiBANTSO1hgphF6UqEPQiCt3wnMPaxaanjMQxjS/iBk1UGXR2EGA==} + '@scalar/json-magic@0.12.17': + resolution: {integrity: sha512-Vw2nrUDIjhvMP6vxFtkiiFlabJ6SyTtfn1BsOxgnr1hIB+/rkngMguiDzl5em21VjyfFGIoADia+QWKM2hdcdA==} engines: {node: '>=22'} - '@scalar/oas-utils@0.18.5': - resolution: {integrity: sha512-GLL0DPEaKkIrDmkwY83RDnCKQRuzw1u6MjGlEX5iVs/LDjQRvWnkAz9A5iUrH/YHr+ike5NIl3/WzaEWiz6x6A==} + '@scalar/oas-utils@0.19.3': + resolution: {integrity: sha512-+h/vLMfGj/mpr5FYgDLFIc+X75sYH2rc2MJNSvFTmmjMeerJDVINEYs+aYOx1Mn9Z3HnoP+qQohoWTJpg2gh7A==} engines: {node: '>=22'} '@scalar/openapi-types@0.9.1': @@ -6604,27 +6751,27 @@ packages: resolution: {integrity: sha512-D5b0rGLLZgmkO9mdW2j/ND1KBlH1u3RCpr87HPxv9P9ZSr6PtM5iLqFOJq0ACiaHjY2mikCrxgDmnUEhTzRpHQ==} engines: {node: '>=22'} - '@scalar/schemas@0.5.0': - resolution: {integrity: sha512-aV0PWqFpQft9a6d4Z6HOTHKDaA+K4AquxSPfBDI1c5f6lMe5l3zViww/iBQA/Yw+V1+Y2iwSVzhuxaktC1UiMg==} + '@scalar/schemas@0.7.1': + resolution: {integrity: sha512-80bxEp4ZOWxOm8kqhPi3kdJ2gipz2lZBSEHStAh3Z6NZPkFAOlZZYnDFwodhY0LwTihgvv4FVNmi64gziM0F1g==} engines: {node: '>=22'} - '@scalar/sidebar@0.9.23': - resolution: {integrity: sha512-2qC5vh1BGhnARF9/psKr5jPbwWyg4RIfflNmh7q/jHxNyeCrHGr1yyUiV8TsWmURR5ZNMiyUPzHaio4hdAcZNw==} + '@scalar/sidebar@0.9.27': + resolution: {integrity: sha512-Y2HBOuUuRSJtB4sf9T5sqZZrsT8QNYP8kWuK8lqzsujf3bv7c4ixUFRNS568EMMjr0U7Zv9xa+LP0RHem3rt4g==} engines: {node: '>=22'} - '@scalar/snippetz@0.9.17': - resolution: {integrity: sha512-BC8ai3E9O5TbJwdGTounh9T6BVSTuYJ/BObZ6wfmXxMK/AL7bDfYED5xBqqzucAGpjVUaN0u796w5EHB82ryrQ==} + '@scalar/snippetz@0.9.20': + resolution: {integrity: sha512-A3gSBYtoTmW3m511d02vUyl/W/eabX5y/EPAwz/u88LTzdwuDrKWbZ8iY+5dkz5e5EErhNOwQDwngmOCAA91hA==} engines: {node: '>=22'} - '@scalar/themes@0.16.0': - resolution: {integrity: sha512-xcd5cdouNKGGI+D3v4SVm5WVLjiLDPT0jBiZrNA6m/h2yDaIeFoXMDrH61aj962Y+P7KR33/2b0xLHgws5Os8Q==} + '@scalar/themes@0.16.2': + resolution: {integrity: sha512-4mAn7z2W5/ASi1OF06dqf04xjxTHnMzESC2E+qVMukJsEsV4kDlYSIfm/g5hwWxhqWsBMjorF9Dtlqd0ycJ92g==} engines: {node: '>=22'} '@scalar/typebox@0.1.3': resolution: {integrity: sha512-lU055AUccECZMIfGA0z/C1StYmboAYIPJLDFBzOO81yXBi35Pxdq+I4fWX6iUZ8qcoHneiLGk9jAUM1rA93iEg==} - '@scalar/types@0.14.0': - resolution: {integrity: sha512-aS1FvHbKhBC51mWqXmhkPe6lmhCd9+u//DQ1YTiCWOz4/fXAdWfwHvW9UZ7AzzcGwGiCo9diE81xV+UvSI1qIg==} + '@scalar/types@0.16.1': + resolution: {integrity: sha512-zzApf0dtEqztdY//3gmRJTgySGMpKnAVqcZltAzt95yuQPXbkSqxXDTitBLd1abeSeik+W6GFTIjffL8K+1wqQ==} engines: {node: '>=22'} '@scalar/use-codemirror@0.14.12': @@ -6643,8 +6790,8 @@ packages: resolution: {integrity: sha512-tpmmG+/xRE2Kn9RpflU3AIyZv08v10+E1ZrJCx7z6+/91zHVxy0M73kC1LT4/8PbYNt85ywyC8+n+D99JdMcGA==} engines: {node: '>=20'} - '@scalar/workspace-store@0.54.4': - resolution: {integrity: sha512-82ffXzHc/TDR6mr9BSzzRkxOaVG8mHQlWLKnPiv8GiFNETgBTQKv9nELEZQpLcG02cJVgVytl+X/TjgZHRDKAw==} + '@scalar/workspace-store@0.55.2': + resolution: {integrity: sha512-/8BfJkave9vmweLdzH7w2TCaUFNcLu1vmE87id0QIi08enOwOExmPsz8YpUtaKLHM2bi/R2Tj/s9Uwp9i8Lttw==} engines: {node: '>=22'} '@schummar/icu-type-parser@1.21.5': @@ -6679,8 +6826,8 @@ packages: peerDependencies: semantic-release: '>=18.0.0' - '@semantic-release/github@12.0.8': - resolution: {integrity: sha512-tej5AAgK5X9wHRoDmYhecMXEHEkFeGOY1XsEblKxu8pIQwahzf1STYyr7iPU6Lpbg6C5I3N2w/ocXrBo+L7jhw==} + '@semantic-release/github@12.0.9': + resolution: {integrity: sha512-ODIqb0V3QqndipryEEiaBxUQCFjvv7Oese5Dt4omMGa60YRNEW0Sx3K+zri0uac2Y6S9nOlMehciWIzvvRCTGQ==} engines: {node: ^22.14.0 || >= 24.10.0} peerDependencies: semantic-release: '>=24.1.0' @@ -7033,69 +7180,69 @@ packages: '@tabler/icons@3.44.0': resolution: {integrity: sha512-Wn0AOZG9sg0L+bjfMqq4eNhC6pQjIrk94LvvWYNYkY8KH8wC3YILRzQlrnVJc4FUeMxH/AK97QsYCX35H3LndA==} - '@tailwindcss/node@4.3.1': - resolution: {integrity: sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==} + '@tailwindcss/node@4.3.2': + resolution: {integrity: sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==} - '@tailwindcss/oxide-android-arm64@4.3.1': - resolution: {integrity: sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==} + '@tailwindcss/oxide-android-arm64@4.3.2': + resolution: {integrity: sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==} engines: {node: '>= 20'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.3.1': - resolution: {integrity: sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==} + '@tailwindcss/oxide-darwin-arm64@4.3.2': + resolution: {integrity: sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==} engines: {node: '>= 20'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.3.1': - resolution: {integrity: sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==} + '@tailwindcss/oxide-darwin-x64@4.3.2': + resolution: {integrity: sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==} engines: {node: '>= 20'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.3.1': - resolution: {integrity: sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==} + '@tailwindcss/oxide-freebsd-x64@4.3.2': + resolution: {integrity: sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==} engines: {node: '>= 20'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1': - resolution: {integrity: sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2': + resolution: {integrity: sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==} engines: {node: '>= 20'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.3.1': - resolution: {integrity: sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==} + '@tailwindcss/oxide-linux-arm64-gnu@4.3.2': + resolution: {integrity: sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-arm64-musl@4.3.1': - resolution: {integrity: sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==} + '@tailwindcss/oxide-linux-arm64-musl@4.3.2': + resolution: {integrity: sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] libc: [musl] - '@tailwindcss/oxide-linux-x64-gnu@4.3.1': - resolution: {integrity: sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==} + '@tailwindcss/oxide-linux-x64-gnu@4.3.2': + resolution: {integrity: sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==} engines: {node: '>= 20'} cpu: [x64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-x64-musl@4.3.1': - resolution: {integrity: sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==} + '@tailwindcss/oxide-linux-x64-musl@4.3.2': + resolution: {integrity: sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==} engines: {node: '>= 20'} cpu: [x64] os: [linux] libc: [musl] - '@tailwindcss/oxide-wasm32-wasi@4.3.1': - resolution: {integrity: sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==} + '@tailwindcss/oxide-wasm32-wasi@4.3.2': + resolution: {integrity: sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -7106,53 +7253,62 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.3.1': - resolution: {integrity: sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==} + '@tailwindcss/oxide-win32-arm64-msvc@4.3.2': + resolution: {integrity: sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==} engines: {node: '>= 20'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.3.1': - resolution: {integrity: sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==} + '@tailwindcss/oxide-win32-x64-msvc@4.3.2': + resolution: {integrity: sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==} engines: {node: '>= 20'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.3.1': - resolution: {integrity: sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==} + '@tailwindcss/oxide@4.3.2': + resolution: {integrity: sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==} engines: {node: '>= 20'} - '@tailwindcss/postcss@4.3.1': - resolution: {integrity: sha512-dNJuNbdEJT/SWRuXTYP1WSamelsz3ztkUsdtWQPjrexysrTpaEPM40P/71knXiXLYEojqPOEGitVLLpPMS5T6A==} + '@tailwindcss/postcss@4.3.2': + resolution: {integrity: sha512-rjVWYCa7Ngbi5AarT6k8TkxUG3Wl1QKzHdIZVsjZSzf36Jmo2IKZt/NHRAwly8oDkbBOH0YTu+CHuf9jPxMc+g==} '@tanstack/match-sorter-utils@8.19.4': resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==} engines: {node: '>=12'} - '@tanstack/query-core@5.101.0': - resolution: {integrity: sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==} + '@tanstack/query-async-storage-persister@5.101.2': + resolution: {integrity: sha512-jf7zvVKZ5q2j/xuhbTIBUpw5xt6cw/ioOFQquxK7fRY0AmMQP8A0JVMOgYoeQLA4PKdeT7RxsuLAF0Lft9oJ8Q==} - '@tanstack/query-devtools@5.101.0': - resolution: {integrity: sha512-MVqw17k08RQtGGLEL654+dX/btbX9p/8WjkznO//zusLTMaObxi3Q+MoFwGVkC9K3tqjn8qrrNhJevXx4fJTeQ==} + '@tanstack/query-core@5.101.2': + resolution: {integrity: sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==} - '@tanstack/query-persist-client-core@5.101.0': - resolution: {integrity: sha512-LH99WepGVLwlLfuOcQcPK7f3Xg/Gf+xlMMIj9xWu/8oQ3egnDzjr+a4HvEmi6PGob5SmGXvmDKZaH5+In9dzjw==} + '@tanstack/query-devtools@5.101.2': + resolution: {integrity: sha512-o+wHcqgN7Pp0s8v1i0UGq/ZrrEKrxdIiMQmKRdYb2w7NPtylYSJ4+wg/tIn71m9DLstwUwdEGAvROdly6HXP6w==} - '@tanstack/react-query-devtools@5.101.0': - resolution: {integrity: sha512-cpZA0+WqKXwrwMfiWZEGGF6QrIWVQFbhBtxqDF5sQsAfrFf47HIE6fiPbQU3wyAUEN2+7UNqLCQe7oG6m3f93w==} + '@tanstack/query-persist-client-core@5.101.2': + resolution: {integrity: sha512-rgMsbvBVpSPDUsprC9FYxojdG0Q1LH6yq1i3DXz2aps4VEqv2l4Msj3YDqIifU3xkl/DrYe9YWntZAJLrM6xTg==} + + '@tanstack/react-query-devtools@5.101.2': + resolution: {integrity: sha512-eU7HctdA9gDjqoERoEdzLbw9DiqnBDfh5+Hu0u26gjqoHJezOpQAuiesDL2VvkU+2cPV76zgv0tMZsOrI4LjnQ==} peerDependencies: - '@tanstack/react-query': ^5.101.0 + '@tanstack/react-query': ^5.101.2 react: ^18 || ^19 - '@tanstack/react-query-next-experimental@5.101.0': - resolution: {integrity: sha512-YzNMm2v+wfPMYsKoD06MLdtDpXcI6AGDRTHxvZurXA2ozBEABQzaBzcMFduriytyMPz1bnkzqcTJ+mcLCejMZA==} + '@tanstack/react-query-next-experimental@5.101.2': + resolution: {integrity: sha512-2gqRwiPfzBHQ+mQQkK9IL8tzDcUrVTd5MkaiKaM0x1qG4OySNDKE9hSsjJ1ywXWO00KaaCvzInWdJfx1yksgwg==} peerDependencies: - '@tanstack/react-query': ^5.101.0 + '@tanstack/react-query': ^5.101.2 next: ^13 || ^14 || ^15 || ^16 react: ^18 || ^19 - '@tanstack/react-query@5.101.0': - resolution: {integrity: sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg==} + '@tanstack/react-query-persist-client@5.101.2': + resolution: {integrity: sha512-19UpaRtf0lvB2QAJ2MoixxtGf3osMOd508g99EsttUj4+3eLs+ulnNgYjB7AkQMHrRlcbVXqpVNqWkB4a7g8Zw==} + peerDependencies: + '@tanstack/react-query': ^5.101.2 + react: ^18 || ^19 + + '@tanstack/react-query@5.101.2': + resolution: {integrity: sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg==} peerDependencies: react: ^18 || ^19 @@ -7184,14 +7340,14 @@ packages: peerDependencies: vue: ^2.7.0 || ^3.0.0 - '@testcontainers/mysql@12.0.3': - resolution: {integrity: sha512-xaub3gaAQKR4Q2/9d8B+81XM8LLTQHPOncCL3Ved04JUUIeTobMt3J36JXv/rvEpnrMpphlKYlmQU0aq48HeZA==} + '@testcontainers/mysql@12.0.4': + resolution: {integrity: sha512-XZWh/L6EN8XQa5lsBdESDolDmyUA56bDgYh5SicLzzC2b4jho4lc0yA0GOOVX/bd0zK6VQK6hmDDJE+T3rFrdA==} - '@testcontainers/postgresql@12.0.3': - resolution: {integrity: sha512-y0l/ItWxNfdMU783EOZ66lMq90uuuXPqE+fo1jNSRfMbtfUq/9Ez/wluDxxaS+D/i3I0juep+jmz/WVuip8i9Q==} + '@testcontainers/postgresql@12.0.4': + resolution: {integrity: sha512-a/pLU6j5lpKKAlUTPwqweqMGhOSjgTSb6HBX69TOrXn32ifU37nnQDmNFTj8ddOAw+BQL9oTRkeOxVbZkqhgZA==} - '@testcontainers/redis@12.0.3': - resolution: {integrity: sha512-hN3YedOSb6quFzG1IM4j/r/M635GcaYOxzbTuiSN7676BZHrx1VPl+xll3ojk8ds6TvenrfO37lrEVJYN2URlA==} + '@testcontainers/redis@12.0.4': + resolution: {integrity: sha512-44Ov3Lcptb3KqljcCy6xIJ2VxNsN+3Kw/YpBiotZpOcI4iLgLNmcOgvCFPVo+vDoh4rCfgtNUEbIpL5Mu7oRuw==} '@tiptap/core@3.27.1': resolution: {integrity: sha512-rV6Qn4wmC6BxfF+4mu6bqGWj9vA4oXXhsrpXaJL2uhjxeHAGofjwcHof2X84VYzeyXgdlsGmqKie4TAppVXZUQ==} @@ -7403,21 +7559,21 @@ packages: '@tiptap/starter-kit@3.27.1': resolution: {integrity: sha512-vfxRsqW8rCc0k4pzo0ilU3wobVi2wqVj88VZI2SlgZlNnUAkrDGDIAph7CTa9k9fshV+O1ivpEgPC5yC046jow==} - '@trpc/client@11.17.0': - resolution: {integrity: sha512-KpJBFrbKTDeVCFv/3ckL1XBBH5Yssn8hethI/rUy7GIpTj+VzjtPjykDqJpzobuVOz+d26cXCSu1t4I6MYI5Zg==} + '@trpc/client@11.18.0': + resolution: {integrity: sha512-wOqeg3Fvl25V1ZisQhUD3K8G60ZJDlSGJNSyeXrLH24xAo5w6GSR2Kzb1cSNY9Y+IQ2YZvYGZstBU+V/ulo/ow==} hasBin: true peerDependencies: - '@trpc/server': 11.17.0 + '@trpc/server': 11.18.0 typescript: '>=5.7.2' - '@trpc/next@11.17.0': - resolution: {integrity: sha512-EjwMkyamGjf5aslgfYB70hMKs4cgmsjSJbMinmuXktkGvbmdW54VbV04Sk4o4QTY5b//Rns9dz+ku8DN6lVhvw==} + '@trpc/next@11.18.0': + resolution: {integrity: sha512-ocwbruAWMGX9hY3HFg86X4jAcoF2v+xx+A2jDn72SbttRRG2hXR+XKPjrLc1dDJC0oi+/2DJEbL14+k1pyY5og==} hasBin: true peerDependencies: '@tanstack/react-query': ^5.59.15 - '@trpc/client': 11.17.0 - '@trpc/react-query': 11.17.0 - '@trpc/server': 11.17.0 + '@trpc/client': 11.18.0 + '@trpc/react-query': 11.18.0 + '@trpc/server': 11.18.0 next: '*' react: '>=16.8.0' react-dom: '>=16.8.0' @@ -7428,70 +7584,70 @@ packages: '@trpc/react-query': optional: true - '@trpc/react-query@11.17.0': - resolution: {integrity: sha512-AGcl5YAF8NnhBmyJ6PqJqKb1M5VTGSoNRNqJ3orct4o4epdcg0GWhW+qT9q6gPzs/2ImIwYCdfFpgNGdZ9yLHA==} + '@trpc/react-query@11.18.0': + resolution: {integrity: sha512-C1+Wwm2pCeUJucI+bnFpxGYjNuvV+ko1BC1T9tUxBVdrhHRCdn9ubxdevdLSAa49XRJRJiZnSuzl3Ys/yvs1vg==} peerDependencies: '@tanstack/react-query': ^5.80.3 - '@trpc/client': 11.17.0 - '@trpc/server': 11.17.0 + '@trpc/client': 11.18.0 + '@trpc/server': 11.18.0 react: '>=18.2.0' typescript: '>=5.7.2' - '@trpc/server@11.17.0': - resolution: {integrity: sha512-jbAOUe0PpUTCYqziyu+8vYXZdDXPudZgnEhWCQ2NjKnVEjfE93RqHTt1oycZJv/HNf51YlRXfEEwSIAbb161rw==} + '@trpc/server@11.18.0': + resolution: {integrity: sha512-JAvXOuNTxgXjIDfQaOvDq1j66LMNfDJUH1IU7Slfn8EvRv2EkH6ehu3A7zpYhjO0syHHiYg77v2lG2JFJgvw7Q==} hasBin: true peerDependencies: typescript: '>=5.7.2' - '@trpc/tanstack-react-query@11.17.0': - resolution: {integrity: sha512-OFxjvCgisP0yaCj7lgP6qPaFwJvJDgnrAUxH3cNIPPR89TpYbndn5vsdKNkc7EnvER9b6wq9Yz8aCcPcOBpDXg==} + '@trpc/tanstack-react-query@11.18.0': + resolution: {integrity: sha512-dm5xIlN0SEzJAbQ34EHdb1Kgd/zWZ63ZMviQEw3WCWCIPz8MjLKAhquncPcn+YxFUJa+Qi4qatdGFymmM4HmAQ==} hasBin: true peerDependencies: '@tanstack/react-query': ^5.80.3 - '@trpc/client': 11.17.0 - '@trpc/server': 11.17.0 + '@trpc/client': 11.18.0 + '@trpc/server': 11.18.0 react: '>=18.2.0' typescript: '>=5.7.2' '@tsconfig/docusaurus@2.0.9': resolution: {integrity: sha512-0jVxZCgy2v7TnJrW9kVNikNwJHeiWb68Zhiiw2vHw4tzBFSP5vS2zn3O5EY2oPEVz5dXZRkK7MnWG3Ay5q0mIg==} - '@turbo/darwin-64@2.9.18': - resolution: {integrity: sha512-9f27peFu16ur8c0v9nUFUEyBnbKuuFsUTjHFWfmwGfzySBXbHwzU44QhZon6Mznz0cHsIr3984NQj/bVrnGSRw==} + '@turbo/darwin-64@2.10.2': + resolution: {integrity: sha512-wBM3ObqOWnKUDmg7QfUFDkDHPFUAJmrYlYqmEM8jMPAPA/I6wRJIbWimeQUqhOiQ8xPKhzyWM+xaiUP0wz8FEQ==} cpu: [x64] os: [darwin] - '@turbo/darwin-arm64@2.9.18': - resolution: {integrity: sha512-9A6TMRq/Ib+QnbhLlgkhOm+624wO4pzSQ/yQviQfWHOlFvaYxdnIAYmu2H6TS6y7kSVL0DvzNe04NbESTOzFVQ==} + '@turbo/darwin-arm64@2.10.2': + resolution: {integrity: sha512-/Cq0joWnuMjDPfhjbFP4sv+C/7gkQ415zlaO4XUzD5EZxbtrKgXKvuuydMvogG8GeUnN1aDltW71RlmEfpjbyw==} cpu: [arm64] os: [darwin] - '@turbo/gen@2.9.18': - resolution: {integrity: sha512-9Ry3V2eqFANYI7A5dyjehq2EOuLTY30XQSg4aDR7F3cJOuiP/Ad2KXwrxD3AnwNDkuSDVbJjlbES7yfJ0y7dhw==} + '@turbo/gen@2.10.2': + resolution: {integrity: sha512-euo5g9gAjMaWt0sl8Xg2ThahSIuGvK/hqciXt8sthH+HrifzjKa9TQaIVtdTpFzsTKfOqBlUvd5O+aPrEnvxqQ==} hasBin: true - '@turbo/linux-64@2.9.18': - resolution: {integrity: sha512-zCdIDtz69AnbYh913elJRRoF3QY5aa2HNnf+4rAkc7bQ+tWujiDkCNV7stazOUPggaDvhKIf2Z87qHftTeXSkw==} + '@turbo/linux-64@2.10.2': + resolution: {integrity: sha512-mMsf5IIhiKuceEXNstd25IbadjBXZ0amxzFOqliEzJX6HyeeHdBQPVSY583PWqYDyqM/FB8d5ZjkthfBSeuH3Q==} cpu: [x64] os: [linux] - '@turbo/linux-arm64@2.9.18': - resolution: {integrity: sha512-Va1kXI04naMgYwqv/5Dfa36dTDx8015U7oaQAjrXa45ua9OoFjSV4OmvkML4EmXvUclQHCiBRbY8bvd0jV7eAg==} + '@turbo/linux-arm64@2.10.2': + resolution: {integrity: sha512-Wcng1i2kaKmXutmwxT9MUoYZvdaIekXAdlGr4+0TpgbhGLw7nDuEcRBFrxb5BbRoX1d1q8SpdRxLc45TvDZIdQ==} cpu: [arm64] os: [linux] - '@turbo/windows-64@2.9.18': - resolution: {integrity: sha512-m0kDhZANxSNz9ck1ybogFscHabriAsp4eDFNrN/1H5WrgTF7b3VlcPZnhuO3v2+E2KnCbeAc+UUT10BZZHdDKw==} + '@turbo/windows-64@2.10.2': + resolution: {integrity: sha512-SsNhM7Ho7EpAdwtrJKBOic9Hso23vu6Dp0gAfLOvUFjPzurr/sGQlXZEvr6z89ne4RDOypTwz5CBDrixpMKtXw==} cpu: [x64] os: [win32] - '@turbo/windows-arm64@2.9.18': - resolution: {integrity: sha512-nUdR8WqoomUys9iIQmG45TMiizJ+5BV8egSeLLZba/AWblyp3fVBcIH1kSE58OtK4g2YzbMJEth6Ttv9w5rqMA==} + '@turbo/windows-arm64@2.10.2': + resolution: {integrity: sha512-Gf+S7ICAdimT/n02bOuVWKvhHnct/HYjZg3oBNIz5hZ9ZyWHbQim9J3P5Qip8WpX0ksxF7eaBVziJCuLnjhqDg==} cpu: [arm64] os: [win32] - '@tybys/wasm-util@0.10.2': - resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} '@types/adm-zip@0.5.8': resolution: {integrity: sha512-RVVH7QvZYbN+ihqZ4kX/dMiowf6o+Jk1fNwiSdx0NahBJLU787zkULhGhJM8mf/obmLGmgdMM0bXsQTmyfbR7Q==} @@ -7884,8 +8040,8 @@ packages: '@videojs/xhr@2.7.0': resolution: {integrity: sha512-giab+EVRanChIupZK7gXjHy90y3nncA2phIOyG3Ne5fvpiMJzvqYwiTOnEVW2S4CoYcuKJkomat7bMXA/UoUZQ==} - '@vitejs/plugin-react@6.0.2': - resolution: {integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==} + '@vitejs/plugin-react@6.0.3': + resolution: {integrity: sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 @@ -7981,7 +8137,7 @@ packages: resolution: {integrity: sha512-SDobKBbPIOe0cVL7QxMzGkuUGHvWTdihi9zOrrWaWUgFKe15cwEcwfWmgrcNzjT6kHnNmWuTajPHoIzUjYNYYQ==} peerDependencies: async-validator: ^4 - axios: '>=1.18.0' + axios: '>=1.18.1' change-case: ^5 drauu: ^0.4 focus-trap: ^7 @@ -8139,8 +8295,8 @@ packages: resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} engines: {node: '>= 10.0.0'} - adm-zip@0.5.17: - resolution: {integrity: sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==} + adm-zip@0.5.18: + resolution: {integrity: sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==} engines: {node: '>=12.0'} aes-decrypter@4.0.2: @@ -8417,9 +8573,6 @@ packages: bare-events: optional: true - base-64@1.0.0: - resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} - base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -8514,8 +8667,8 @@ packages: zod: optional: true - better-sqlite3@12.10.0: - resolution: {integrity: sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==} + better-sqlite3@12.11.1: + resolution: {integrity: sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==} engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x} bidi-js@1.0.3: @@ -8569,6 +8722,10 @@ packages: resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} + brace-expansion@5.0.7: + resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -8927,9 +9084,9 @@ packages: resolution: {integrity: sha512-CLf+zr6St0wIxos4bmaKHRXWAcsCXrJU6F4VdNDrGRK3B8LDLKoX3zuMV5GhtbGkVR/LohZ6MT6im43vZLSjmA==} engines: {node: '>=18'} - conventional-changelog-conventionalcommits@9.3.1: - resolution: {integrity: sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw==} - engines: {node: '>=18'} + conventional-changelog-conventionalcommits@10.2.0: + resolution: {integrity: sha512-UtlM9GqolY7OmlQh5L/UEVoKsTUpTgUVy1PU8JN5gl5Ydaejb7WRklGliG1SKPxxj7hzA173eG3Kt5fYWE2pmg==} + engines: {node: '>=22'} conventional-changelog-writer@8.0.0: resolution: {integrity: sha512-TQcoYGRatlAnT2qEWDON/XSfnVG38JzA7E0wcGScu7RElQBkg9WWgZd1peCWFcWDh1xfb2CfsrcvOn1bbSzztA==} @@ -8973,6 +9130,10 @@ packages: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + cookie@2.0.1: + resolution: {integrity: sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==} + engines: {node: '>=22'} + cookies@0.9.1: resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==} engines: {node: '>= 0.8'} @@ -9525,6 +9686,10 @@ packages: resolution: {integrity: sha512-C52mvJ+7lcyhWNfrzVfFsbTrBfy/ezE9FGEYLpu17FUeBcCkxERk9nN7uDl/478ynDiQ4U+5DbQC2vENHkVEtQ==} engines: {node: '>= 14.17'} + dockerode@5.0.1: + resolution: {integrity: sha512-avsq/xk4YPIrn0CgleX5bjT9Y8IT1p9PxrNQ++RBQ2WEyFfHCTDsT9kmyxz+H/axnjAwg8wJWEIuPGOUuNupiA==} + engines: {node: '>= 14.17'} + docusaurus-plugin-image-zoom@3.0.1: resolution: {integrity: sha512-mQrqA99VpoMQJNbi02qkWAMVNC4+kwc6zLLMNzraHAJlwn+HrlUmZSEDcTwgn+H4herYNxHKxveE2WsYy73eGw==} peerDependencies: @@ -9840,6 +10005,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -10042,12 +10212,16 @@ packages: fast-xml-builder@1.2.0: resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} + fast-xml-parser@5.8.0: + resolution: {integrity: sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg==} + hasBin: true + fast-xml-parser@5.9.3: resolution: {integrity: sha512-brCNCeScma/kqa54J4PIDriSSSLssRkuYaUCpvHJulGc3HGI/xxKUCTDcYkAdqJsyb//ydpbxecjC3hB9+tb/g==} hasBin: true - fastify@5.8.5: - resolution: {integrity: sha512-Yqptv59pQzPgQUSIm87hMqHJmdkb1+GPxdE6vW6FRyVE9G86mt7rOghitiU4JHRaTyDUk9pfeKmDeu70lAwM4Q==} + fastify@5.9.0: + resolution: {integrity: sha512-VMS5lE0zj+MZlJpQa3Qv5iGjfun0H2N7VRgoBwpcTNQ2bdIQpv7fDpb+HGteGbicBsGkzGS+X+hdx9mmrfWuHQ==} fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -10118,8 +10292,8 @@ packages: resolution: {integrity: sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==} engines: {node: '>=14.16'} - find-my-way@9.5.0: - resolution: {integrity: sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==} + find-my-way@9.6.0: + resolution: {integrity: sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==} engines: {node: '>=20'} find-up-simple@1.0.1: @@ -10263,9 +10437,9 @@ packages: get-own-enumerable-property-symbols@3.0.2: resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} - get-port@7.2.0: - resolution: {integrity: sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg==} - engines: {node: '>=16'} + get-port@5.1.1: + resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} + engines: {node: '>=8'} get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} @@ -10662,8 +10836,8 @@ packages: peerDependencies: postcss: ^8.1.0 - icu-minify@4.13.0: - resolution: {integrity: sha512-SIFMeUHZJjzS5RvIGvybKvWoHjDm9cGVEs2EpJ8PmywOdJLWyblPm7TdPLLoUtkJtwQD7iGhl2WMptZ+N0on+w==} + icu-minify@4.13.1: + resolution: {integrity: sha512-nFYW2im0WJ3RUVZwabd71J8QTZRtkK1xxZBY7klg7a6KS/os17LZSj9q1VhbRSnk3S8Mv2I7F1izr/aEJ6cUsw==} identifier-regex@1.0.1: resolution: {integrity: sha512-ZrYyM0sozNPZlvBvE7Oq9Bn44n0qKGrYu5sQ0JzMUnjIhpgWYE2JB6aBoFwEYdPjqj7jPyxXTMJiHDOxDfd8yw==} @@ -10683,12 +10857,12 @@ packages: immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} - immer@10.2.0: - resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} - immer@11.1.4: resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==} + immer@11.1.9: + resolution: {integrity: sha512-sc/z0Cyti70bZa0ZU4sWfAElfovFb9Ni8tArJZLuklYWxegPiK3pDOql1Rq5H0FIRAW9LSQRG6OX4KqBldbhBA==} + immutable@5.1.5: resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==} @@ -11020,6 +11194,7 @@ packages: jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true joi@17.13.3: resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} @@ -11355,7 +11530,6 @@ packages: loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -11381,8 +11555,8 @@ packages: resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} - lucide-react@1.21.0: - resolution: {integrity: sha512-reEZMXq8Qdd5jg5XYkQ5TR1fB/GiQ7ih4vcrthYDtgjSDwh0i6/YLiGjsWsIwgN49gpAnd4J2elSNzncMEEUUQ==} + lucide-react@1.23.0: + resolution: {integrity: sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -11850,14 +12024,10 @@ packages: nanoid@3.3.12: resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - - nanoid@5.1.11: - resolution: {integrity: sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==} - engines: {node: ^18 || >=20} hasBin: true - nanoid@5.1.15: - resolution: {integrity: sha512-kBg3RpGtIe+RpTbyXwoI6pk5yD7KUiI3sygUqgeBMRst42KmhB4RZC7eiO9Wa1HIpaCCtpE2DJ6OI4Wi5ebwFw==} + nanoid@5.1.16: + resolution: {integrity: sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==} engines: {node: ^18 || >=20} hasBin: true @@ -11905,11 +12075,11 @@ packages: nodemailer: optional: true - next-intl-swc-plugin-extractor@4.13.0: - resolution: {integrity: sha512-6S/fJI0KXvLCL8nhBo9P8eGaJPzmwJBTCzX0NaUIj0VyU8U89d//T+vjMLdNIXl5MlLaYH7B9MbAjb8Mvu+tqQ==} + next-intl-swc-plugin-extractor@4.13.1: + resolution: {integrity: sha512-RhlH2DR1ViEXzcX7G3tDXAvzNrBL2Ph54Hq/q/9oP9eXQV/okh3UQpA/lx2k9U5Ck83CZOSgH0eFTNi5U+zyXw==} - next-intl@4.13.0: - resolution: {integrity: sha512-OvNq2v5XLx4EkQOsAhVE9g+6zdb83XHusADCXXtIW4LILYnjEVaeINdr1lkVWKSjzwNUiMSlH5N4K0OQTRiv6A==} + next-intl@4.13.1: + resolution: {integrity: sha512-aS8KTA+nNhSNJJBlIhxgvU135WzoObwzFwav4wTDti/Gmhxqe0fs/Q343igo8Z7HGqPB/xgmoagwySZAlHmIfA==} peerDependencies: next: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0 @@ -11918,8 +12088,8 @@ packages: typescript: optional: true - next@16.2.9: - resolution: {integrity: sha512-MEOJiq/UvuezAdqVSceHbqDgZt1kDw2tpGVOlsdIoJsQdbN2JY2hpVG4xnXGkbdJUOEWhnRfiu/O4Hpc9Juwww==} + next@16.2.10: + resolution: {integrity: sha512-2som5AVXb3kE6Yjine3/mNbBayYF58eguBWIVVUdr1y/L426xyVEgYxgBG+1QC34P2x5E+tcDup6XkuOAX3dCA==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -12182,8 +12352,8 @@ packages: outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} - oxfmt@0.55.0: - resolution: {integrity: sha512-jSj2wCTakwgPMxkfiVZX0jf+nX+Nz6xlyAZjqNE0qXTFdCBPYlP6JAN+ODjmealw7DXBjOzYbdsqwBMAZnPZ6A==} + oxfmt@0.57.0: + resolution: {integrity: sha512-ZB7Bi+rGDSqmVIo9jwcLyFgjxXvQhDdU+jx+ZrVy6VRiVXK2+CHc4hO3J4dUQjHe7V0ymHB+MDuv5z+NhK07HA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -12195,8 +12365,8 @@ packages: vite-plus: optional: true - oxlint@1.70.0: - resolution: {integrity: sha512-D6JgHtzkhRwvEC+A0Nw5AEc5bk8x5i1pHzvZIEf/a0C4hOzmAACNGtkDGPyFaxxX3ZVGxCPeig3P3rMM8XU3/g==} + oxlint@1.72.0: + resolution: {integrity: sha512-1rhdZIP/EvoI91ABIwNU5Q8+bWf8mjrS5UzIOZld4d4bXxJvtlUhlQvaoTogIGin/qdErMOrwaIJvCSIAKTLhA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -12449,10 +12619,6 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch-browser@2.2.6: - resolution: {integrity: sha512-0ypsOQt9D4e3hziV8O4elD9uN0z/jtUEfxVRtNaAAtXIyUx9m/SzlO020i8YNL2aL/E6blOvvHQcin6HZlFy/w==} - engines: {node: '>=8.6'} - picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -12495,13 +12661,13 @@ packages: resolution: {integrity: sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==} engines: {node: '>=16.0.0'} - playwright-core@1.61.0: - resolution: {integrity: sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==} + playwright-core@1.61.1: + resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} engines: {node: '>=18'} hasBin: true - playwright@1.61.0: - resolution: {integrity: sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==} + playwright@1.61.1: + resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} engines: {node: '>=18'} hasBin: true @@ -12938,6 +13104,10 @@ packages: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.16: + resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} + engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} @@ -12958,12 +13128,17 @@ packages: resolution: {integrity: sha512-Ray65LYEJrMMqDtsBUBXunEVP/g4wtATvq/xz9rchUoLy/9mSkkFgUko/8DVtGxgiP3vivpFMgfb9HpCuDrBHg==} engines: {node: '>=10.15.1'} - posthog-js@1.391.2: - resolution: {integrity: sha512-q0DZN6ljchSnAFJIXf+sQFTPlsLjTlRa+TvrL+QRb6413BGtib/MNiQy1bnwLKt8KR+f6xJYvkqdLyty9s4Aww==} + posthog-js@1.396.5: + resolution: {integrity: sha512-huW/8R151I08i/tkOaAv3Q/yWVqzhqPj3SxyhpgNLB/xysaNeV1ibp6DHzbRWV5D7FRM48Zg6Icxsm4q0irEtg==} - posthog-node@4.18.0: - resolution: {integrity: sha512-XROs1h+DNatgKh/AlIlCtDxWzwrKdYDb2mOs58n4yN8BkGN9ewqeQwG5ApS4/IzwCb7HPttUkOVulkYatd2PIw==} - engines: {node: '>=15.0.0'} + posthog-node@5.39.4: + resolution: {integrity: sha512-+fCQ7htBFRQQFbIzl1T0TA7bDwYyaB9XP308ZFMCUoB5LzTzOFxBa6TYVrxdH/VQl43WXTp6sf0QsG2Z4XlNBg==} + engines: {node: ^20.20.0 || >=22.22.0} + peerDependencies: + rxjs: ^7.0.0 + peerDependenciesMeta: + rxjs: + optional: true preact-render-to-string@6.5.11: resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==} @@ -13157,9 +13332,6 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - rate-limiter-flexible@8.1.0: - resolution: {integrity: sha512-J+4xBdVboibP1h0Imn4nFoCLT+UM9Os9vJaWaRWkLsQxS7jrhLJeLlmzP5hyCEsLwtgFIIY5KcWiJGyyVTMaKg==} - raw-body@2.5.3: resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} engines: {node: '>= 0.8'} @@ -13355,8 +13527,8 @@ packages: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} - recharts@3.8.1: - resolution: {integrity: sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==} + recharts@3.9.1: + resolution: {integrity: sha512-WMcwlXcB7l+BbxiEdyClkG+1sxrMHNZpzT577LEvU4+rXPd8oTAy1wXk72hnk2KOOmxuLvw3z5DtXT7HEAydtg==} engines: {node: '>=18'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -13507,6 +13679,9 @@ packages: reselect@5.1.1: resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + reselect@5.2.0: + resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==} + reserved-identifiers@1.2.0: resolution: {integrity: sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==} engines: {node: '>=18'} @@ -13568,8 +13743,8 @@ packages: robust-predicates@3.0.3: resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} - rolldown@1.0.3: - resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + rolldown@1.1.4: + resolution: {integrity: sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -14027,6 +14202,9 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strnum@2.3.0: + resolution: {integrity: sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==} + strnum@2.4.1: resolution: {integrity: sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==} @@ -14115,8 +14293,8 @@ packages: '@swc/core': ^1.2.147 webpack: '>=2' - swiper@12.2.0: - resolution: {integrity: sha512-K8uXsBZU6ME97Ia3xbBge8IRCnR1lOmIILzvY/jGVic7dSTQ530s3uO8RvXbPUtkkXLWIwmZLRPbtDxRWVAFdg==} + swiper@14.0.1: + resolution: {integrity: sha512-dibQY282S5rn6S9Ba+GTIvofyY4Qujy526a0cPaJnjR2S18Z7HMGKZBr8k/3ukboeKW/ig5h0q//7CYip0MCQw==} engines: {node: '>= 4.7.0'} swrv@1.2.0: @@ -14137,8 +14315,8 @@ packages: tailwind-merge@3.5.0: resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} - tailwindcss@4.3.1: - resolution: {integrity: sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==} + tailwindcss@4.3.2: + resolution: {integrity: sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==} tapable@2.3.3: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} @@ -14150,6 +14328,9 @@ packages: tar-fs@3.1.2: resolution: {integrity: sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==} + tar-fs@3.1.3: + resolution: {integrity: sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==} + tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} @@ -14212,8 +14393,8 @@ packages: resolution: {integrity: sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==} engines: {node: '>=10'} - testcontainers@12.0.3: - resolution: {integrity: sha512-6vfiL9CWIX0rWhTJitzrzHcv4Q/sUfUdBV12jMz6HZ58Lz6ZtXVecO6jljd1O3ookEWQHNICiYoYNEPpDLzuRw==} + testcontainers@12.0.4: + resolution: {integrity: sha512-QIR/8xF1+F/26cIM+9B4yyxNTbKJxAv3hygZyhPRgZ8Q2AhlPZjDdpXRuk16V37X4bgJRI3hXFhoEICMBA7Adg==} text-decoder@1.2.0: resolution: {integrity: sha512-n1yg1mOj9DNpk3NeZOx7T6jchTbyJS3i3cucbNN6FcdPriMZx7NsgrGpWWdWZZGxD7ES1XB+3uoqHMgOKaN+fg==} @@ -14390,8 +14571,8 @@ packages: typescript: optional: true - tsdav@2.2.2: - resolution: {integrity: sha512-/uC/RItcdYVxUj8b2gT4CorAkUmDFUuzhgS4XVT+OVzGRE80hH5ldubcWH13dHa2T9rNaXu6UIEWHlQ1UL7H1Q==} + tsdav@2.3.0: + resolution: {integrity: sha512-1zxaMcHw/ISDZc+owPykCZqXtJjZSWZr1Vz1ycw/X+8h48pYbUVmTFLl5hRKFx+pMWSYpCsq8rxdYdlpRep7Mw==} engines: {node: '>=18'} tslib@1.14.1: @@ -14425,8 +14606,8 @@ packages: resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} - turbo@2.9.18: - resolution: {integrity: sha512-bwabv6PupzeavybzEoArBAkwq5fnzwf8OFnRtpHwnviFWuwJPFxtyH+aVp36TmIqK3aYYgtTJ3J0m2ysxxSzQg==} + turbo@2.10.2: + resolution: {integrity: sha512-wTExrNrRjB8qzIcg+ZLm0A3GFNLDsWNwdS/RBXB0FPrBDyzk3i96Yx+TxWZC7a0k1SIreFB8ciUbxjmEqTH8IQ==} hasBin: true tweetnacl@0.14.5: @@ -14448,6 +14629,10 @@ packages: resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==} engines: {node: '>=20'} + type-fest@5.7.0: + resolution: {integrity: sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==} + engines: {node: '>=20'} + type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -14493,16 +14678,12 @@ packages: resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} engines: {node: '>=20.18.1'} - undici@7.27.1: - resolution: {integrity: sha512-UDdpiex+mzigiyrXrGbiUaF4HzTNhKbh2vRNFaTMzcqmLIPrZxaCtwo/1TMSuWoM1Xz3WiTo9KdgI3kRqYzJGg==} + undici@7.28.0: + resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} engines: {node: '>=20.18.1'} - undici@7.27.2: - resolution: {integrity: sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==} - engines: {node: '>=20.18.1'} - - undici@8.4.1: - resolution: {integrity: sha512-RNHlB4fxZK0IrkhBsxhlbx7s8kFWwr7rzzOqj5nvZugw3ig3RsB7KW3zVlV0eu8POl+rx5d1hmL7rRg0z1owow==} + undici@8.6.0: + resolution: {integrity: sha512-l2FlC6I510GawyEd1qgcE/okihKrzy+BRTEBlu6T0fdbM9m5yxtIH5Oa3ysRsH0zC4EhmWUEaSDsy2QngBeRlw==} engines: {node: '>=22.19.0'} unhead@2.1.15: @@ -14642,8 +14823,8 @@ packages: peerDependencies: react: '>=16.13' - use-intl@4.13.0: - resolution: {integrity: sha512-fAFDrWaASxlhXOipcOyb5VDD+YONqj6+8O8EcG/J7RBoOUF3A8YahRWLN+mBxYMrlMQB8N6Voqk5X+YC+HSL0A==} + use-intl@4.13.1: + resolution: {integrity: sha512-UU5C3zAC7yVg3m7rq5C8VF5J5jhAfvS19Wi9bPNCB9xB7jQYBsUcrqfdxs4Mxl9XR3x6BDB5K++iAw7/rcm3gg==} peerDependencies: react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0 @@ -14727,15 +14908,15 @@ packages: vite-tsconfig-paths@6.1.1: resolution: {integrity: sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==} peerDependencies: - vite: '>=8.0.16' + vite: '>=8.1.3' - vite@8.0.16: - resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} + vite@8.1.3: + resolution: {integrity: sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.18 + '@vitejs/devtools': ^0.3.0 esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 @@ -15036,9 +15217,6 @@ packages: resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} engines: {node: '>=18'} - xcase@2.0.1: - resolution: {integrity: sha512-UmFXIPU+9Eg3E9m/728Bii0lAIuoc+6nbrNUKaRPJOFp91ih44qqGlWtxMB6kXFrRD6po+86ksHM5XHCfk6iPw==} - xdg-basedir@5.1.0: resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} engines: {node: '>=12'} @@ -16197,12 +16375,12 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.0 - '@better-auth/drizzle-adapter@1.6.15(@better-auth/core@1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1))': + '@better-auth/drizzle-adapter@1.6.15(@better-auth/core@1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1))': dependencies: '@better-auth/core': 1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) '@better-auth/utils': 0.4.1 optionalDependencies: - drizzle-orm: 0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1) + drizzle-orm: 0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1) '@better-auth/kysely-adapter@1.6.15(@better-auth/core@1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)(kysely@0.29.2)': dependencies: @@ -16345,6 +16523,8 @@ snapshots: '@colors/colors@1.6.0': {} + '@conventional-changelog/template@1.2.0': {} + '@csstools/cascade-layer-name-parser@2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) @@ -16399,272 +16579,272 @@ snapshots: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-alpha-function@1.0.1(postcss@8.5.15)': + '@csstools/postcss-alpha-function@1.0.1(postcss@8.5.16)': dependencies: '@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 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.15) - '@csstools/utilities': 2.0.0(postcss@8.5.15) - postcss: 8.5.15 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.16) + '@csstools/utilities': 2.0.0(postcss@8.5.16) + postcss: 8.5.16 - '@csstools/postcss-cascade-layers@5.0.2(postcss@8.5.15)': + '@csstools/postcss-cascade-layers@5.0.2(postcss@8.5.16)': dependencies: '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0) - postcss: 8.5.15 + postcss: 8.5.16 postcss-selector-parser: 7.1.0 - '@csstools/postcss-color-function-display-p3-linear@1.0.1(postcss@8.5.15)': + '@csstools/postcss-color-function-display-p3-linear@1.0.1(postcss@8.5.16)': dependencies: '@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 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.15) - '@csstools/utilities': 2.0.0(postcss@8.5.15) - postcss: 8.5.15 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.16) + '@csstools/utilities': 2.0.0(postcss@8.5.16) + postcss: 8.5.16 - '@csstools/postcss-color-function@4.0.12(postcss@8.5.15)': + '@csstools/postcss-color-function@4.0.12(postcss@8.5.16)': dependencies: '@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 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.15) - '@csstools/utilities': 2.0.0(postcss@8.5.15) - postcss: 8.5.15 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.16) + '@csstools/utilities': 2.0.0(postcss@8.5.16) + postcss: 8.5.16 - '@csstools/postcss-color-mix-function@3.0.12(postcss@8.5.15)': + '@csstools/postcss-color-mix-function@3.0.12(postcss@8.5.16)': dependencies: '@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 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.15) - '@csstools/utilities': 2.0.0(postcss@8.5.15) - postcss: 8.5.15 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.16) + '@csstools/utilities': 2.0.0(postcss@8.5.16) + postcss: 8.5.16 - '@csstools/postcss-color-mix-variadic-function-arguments@1.0.2(postcss@8.5.15)': + '@csstools/postcss-color-mix-variadic-function-arguments@1.0.2(postcss@8.5.16)': dependencies: '@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 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.15) - '@csstools/utilities': 2.0.0(postcss@8.5.15) - postcss: 8.5.15 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.16) + '@csstools/utilities': 2.0.0(postcss@8.5.16) + postcss: 8.5.16 - '@csstools/postcss-content-alt-text@2.0.8(postcss@8.5.15)': + '@csstools/postcss-content-alt-text@2.0.8(postcss@8.5.16)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.15) - '@csstools/utilities': 2.0.0(postcss@8.5.15) - postcss: 8.5.15 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.16) + '@csstools/utilities': 2.0.0(postcss@8.5.16) + postcss: 8.5.16 - '@csstools/postcss-contrast-color-function@2.0.12(postcss@8.5.15)': + '@csstools/postcss-contrast-color-function@2.0.12(postcss@8.5.16)': dependencies: '@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 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.15) - '@csstools/utilities': 2.0.0(postcss@8.5.15) - postcss: 8.5.15 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.16) + '@csstools/utilities': 2.0.0(postcss@8.5.16) + postcss: 8.5.16 - '@csstools/postcss-exponential-functions@2.0.9(postcss@8.5.15)': + '@csstools/postcss-exponential-functions@2.0.9(postcss@8.5.16)': 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-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.15 + postcss: 8.5.16 - '@csstools/postcss-font-format-keywords@4.0.0(postcss@8.5.15)': + '@csstools/postcss-font-format-keywords@4.0.0(postcss@8.5.16)': dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.15) - postcss: 8.5.15 + '@csstools/utilities': 2.0.0(postcss@8.5.16) + postcss: 8.5.16 postcss-value-parser: 4.2.0 - '@csstools/postcss-gamut-mapping@2.0.11(postcss@8.5.15)': + '@csstools/postcss-gamut-mapping@2.0.11(postcss@8.5.16)': dependencies: '@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 - postcss: 8.5.15 + postcss: 8.5.16 - '@csstools/postcss-gradients-interpolation-method@5.0.12(postcss@8.5.15)': + '@csstools/postcss-gradients-interpolation-method@5.0.12(postcss@8.5.16)': dependencies: '@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 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.15) - '@csstools/utilities': 2.0.0(postcss@8.5.15) - postcss: 8.5.15 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.16) + '@csstools/utilities': 2.0.0(postcss@8.5.16) + postcss: 8.5.16 - '@csstools/postcss-hwb-function@4.0.12(postcss@8.5.15)': + '@csstools/postcss-hwb-function@4.0.12(postcss@8.5.16)': dependencies: '@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 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.15) - '@csstools/utilities': 2.0.0(postcss@8.5.15) - postcss: 8.5.15 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.16) + '@csstools/utilities': 2.0.0(postcss@8.5.16) + postcss: 8.5.16 - '@csstools/postcss-ic-unit@4.0.4(postcss@8.5.15)': + '@csstools/postcss-ic-unit@4.0.4(postcss@8.5.16)': dependencies: - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.15) - '@csstools/utilities': 2.0.0(postcss@8.5.15) - postcss: 8.5.15 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.16) + '@csstools/utilities': 2.0.0(postcss@8.5.16) + postcss: 8.5.16 postcss-value-parser: 4.2.0 - '@csstools/postcss-initial@2.0.1(postcss@8.5.15)': + '@csstools/postcss-initial@2.0.1(postcss@8.5.16)': dependencies: - postcss: 8.5.15 + postcss: 8.5.16 - '@csstools/postcss-is-pseudo-class@5.0.3(postcss@8.5.15)': + '@csstools/postcss-is-pseudo-class@5.0.3(postcss@8.5.16)': dependencies: '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0) - postcss: 8.5.15 + postcss: 8.5.16 postcss-selector-parser: 7.1.0 - '@csstools/postcss-light-dark-function@2.0.11(postcss@8.5.15)': + '@csstools/postcss-light-dark-function@2.0.11(postcss@8.5.16)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.15) - '@csstools/utilities': 2.0.0(postcss@8.5.15) - postcss: 8.5.15 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.16) + '@csstools/utilities': 2.0.0(postcss@8.5.16) + postcss: 8.5.16 - '@csstools/postcss-logical-float-and-clear@3.0.0(postcss@8.5.15)': + '@csstools/postcss-logical-float-and-clear@3.0.0(postcss@8.5.16)': dependencies: - postcss: 8.5.15 + postcss: 8.5.16 - '@csstools/postcss-logical-overflow@2.0.0(postcss@8.5.15)': + '@csstools/postcss-logical-overflow@2.0.0(postcss@8.5.16)': dependencies: - postcss: 8.5.15 + postcss: 8.5.16 - '@csstools/postcss-logical-overscroll-behavior@2.0.0(postcss@8.5.15)': + '@csstools/postcss-logical-overscroll-behavior@2.0.0(postcss@8.5.16)': dependencies: - postcss: 8.5.15 + postcss: 8.5.16 - '@csstools/postcss-logical-resize@3.0.0(postcss@8.5.15)': + '@csstools/postcss-logical-resize@3.0.0(postcss@8.5.16)': dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-value-parser: 4.2.0 - '@csstools/postcss-logical-viewport-units@3.0.4(postcss@8.5.15)': + '@csstools/postcss-logical-viewport-units@3.0.4(postcss@8.5.16)': dependencies: '@csstools/css-tokenizer': 3.0.4 - '@csstools/utilities': 2.0.0(postcss@8.5.15) - postcss: 8.5.15 + '@csstools/utilities': 2.0.0(postcss@8.5.16) + postcss: 8.5.16 - '@csstools/postcss-media-minmax@2.0.9(postcss@8.5.15)': + '@csstools/postcss-media-minmax@2.0.9(postcss@8.5.16)': 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-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - postcss: 8.5.15 + postcss: 8.5.16 - '@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.5(postcss@8.5.15)': + '@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.5(postcss@8.5.16)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - postcss: 8.5.15 + postcss: 8.5.16 - '@csstools/postcss-nested-calc@4.0.0(postcss@8.5.15)': + '@csstools/postcss-nested-calc@4.0.0(postcss@8.5.16)': dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.15) - postcss: 8.5.15 + '@csstools/utilities': 2.0.0(postcss@8.5.16) + postcss: 8.5.16 postcss-value-parser: 4.2.0 - '@csstools/postcss-normalize-display-values@4.0.1(postcss@8.5.15)': + '@csstools/postcss-normalize-display-values@4.0.1(postcss@8.5.16)': dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-value-parser: 4.2.0 - '@csstools/postcss-oklab-function@4.0.12(postcss@8.5.15)': + '@csstools/postcss-oklab-function@4.0.12(postcss@8.5.16)': dependencies: '@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 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.15) - '@csstools/utilities': 2.0.0(postcss@8.5.15) - postcss: 8.5.15 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.16) + '@csstools/utilities': 2.0.0(postcss@8.5.16) + postcss: 8.5.16 - '@csstools/postcss-position-area-property@1.0.0(postcss@8.5.15)': + '@csstools/postcss-position-area-property@1.0.0(postcss@8.5.16)': dependencies: - postcss: 8.5.15 + postcss: 8.5.16 - '@csstools/postcss-progressive-custom-properties@4.2.1(postcss@8.5.15)': + '@csstools/postcss-progressive-custom-properties@4.2.1(postcss@8.5.16)': dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-value-parser: 4.2.0 - '@csstools/postcss-property-rule-prelude-list@1.0.0(postcss@8.5.15)': + '@csstools/postcss-property-rule-prelude-list@1.0.0(postcss@8.5.16)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.15 + postcss: 8.5.16 - '@csstools/postcss-random-function@2.0.1(postcss@8.5.15)': + '@csstools/postcss-random-function@2.0.1(postcss@8.5.16)': 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-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.15 + postcss: 8.5.16 - '@csstools/postcss-relative-color-syntax@3.0.12(postcss@8.5.15)': + '@csstools/postcss-relative-color-syntax@3.0.12(postcss@8.5.16)': dependencies: '@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 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.15) - '@csstools/utilities': 2.0.0(postcss@8.5.15) - postcss: 8.5.15 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.16) + '@csstools/utilities': 2.0.0(postcss@8.5.16) + postcss: 8.5.16 - '@csstools/postcss-scope-pseudo-class@4.0.1(postcss@8.5.15)': + '@csstools/postcss-scope-pseudo-class@4.0.1(postcss@8.5.16)': dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-selector-parser: 7.1.0 - '@csstools/postcss-sign-functions@1.1.4(postcss@8.5.15)': + '@csstools/postcss-sign-functions@1.1.4(postcss@8.5.16)': 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-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.15 + postcss: 8.5.16 - '@csstools/postcss-stepped-value-functions@4.0.9(postcss@8.5.15)': + '@csstools/postcss-stepped-value-functions@4.0.9(postcss@8.5.16)': 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-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.15 + postcss: 8.5.16 - '@csstools/postcss-syntax-descriptor-syntax-production@1.0.1(postcss@8.5.15)': + '@csstools/postcss-syntax-descriptor-syntax-production@1.0.1(postcss@8.5.16)': dependencies: '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.15 + postcss: 8.5.16 - '@csstools/postcss-system-ui-font-family@1.0.0(postcss@8.5.15)': + '@csstools/postcss-system-ui-font-family@1.0.0(postcss@8.5.16)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.15 + postcss: 8.5.16 - '@csstools/postcss-text-decoration-shorthand@4.0.3(postcss@8.5.15)': + '@csstools/postcss-text-decoration-shorthand@4.0.3(postcss@8.5.16)': dependencies: '@csstools/color-helpers': 5.1.0 - postcss: 8.5.15 + postcss: 8.5.16 postcss-value-parser: 4.2.0 - '@csstools/postcss-trigonometric-functions@4.0.9(postcss@8.5.15)': + '@csstools/postcss-trigonometric-functions@4.0.9(postcss@8.5.16)': 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-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.15 + postcss: 8.5.16 - '@csstools/postcss-unset-value@4.0.0(postcss@8.5.15)': + '@csstools/postcss-unset-value@4.0.0(postcss@8.5.16)': dependencies: - postcss: 8.5.15 + postcss: 8.5.16 '@csstools/selector-resolve-nested@3.1.0(postcss-selector-parser@7.1.0)': dependencies: @@ -16674,9 +16854,9 @@ snapshots: dependencies: postcss-selector-parser: 7.1.0 - '@csstools/utilities@2.0.0(postcss@8.5.15)': + '@csstools/utilities@2.0.0(postcss@8.5.16)': dependencies: - postcss: 8.5.15 + postcss: 8.5.16 '@ctrl/deluge@8.0.0': dependencies: @@ -16783,7 +16963,7 @@ snapshots: - '@algolia/client-search' - algoliasearch - '@docusaurus/babel@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/babel@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@babel/core': 7.29.0 '@babel/generator': 7.29.0 @@ -16795,7 +16975,7 @@ snapshots: '@babel/runtime': 7.29.7 '@babel/traverse': 7.29.0 '@docusaurus/logger': 3.10.1 - '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) babel-plugin-dynamic-import-node: 2.3.3 fs-extra: 11.2.0 tslib: 2.8.1 @@ -16817,34 +16997,34 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/bundler@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': + '@docusaurus/bundler@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': dependencies: '@babel/core': 7.29.0 - '@docusaurus/babel': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/babel': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@docusaurus/cssnano-preset': 3.10.1 '@docusaurus/logger': 3.10.1 - '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - babel-loader: 9.2.1(@babel/core@7.29.0)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)) + '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + babel-loader: 9.2.1(@babel/core@7.29.0)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)) clean-css: 5.3.3 - copy-webpack-plugin: 11.0.0(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)) - css-loader: 6.11.0(@rspack/core@1.7.11(@swc/helpers@0.5.15))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)) - css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(esbuild@0.28.0)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)) - cssnano: 6.1.2(postcss@8.5.15) - file-loader: 6.2.0(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)) + copy-webpack-plugin: 11.0.0(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)) + css-loader: 6.11.0(@rspack/core@1.7.11(@swc/helpers@0.5.15))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)) + css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(esbuild@0.28.0)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)) + cssnano: 6.1.2(postcss@8.5.16) + file-loader: 6.2.0(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)) html-minifier-terser: 7.2.0 - mini-css-extract-plugin: 2.10.2(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)) - null-loader: 4.0.1(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)) - postcss: 8.5.15 - postcss-loader: 7.3.4(postcss@8.5.15)(typescript@6.0.3)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)) - postcss-preset-env: 10.6.1(postcss@8.5.15) - terser-webpack-plugin: 5.6.0(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)) + mini-css-extract-plugin: 2.10.2(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)) + null-loader: 4.0.1(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)) + postcss: 8.5.16 + postcss-loader: 7.3.4(postcss@8.5.16)(typescript@6.0.3)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)) + postcss-preset-env: 10.6.1(postcss@8.5.16) + terser-webpack-plugin: 5.6.0(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.16)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)) tslib: 2.8.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)) - webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15) - webpackbar: 7.0.0(@rspack/core@1.7.11(@swc/helpers@0.5.15))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16) + webpackbar: 7.0.0(@rspack/core@1.7.11(@swc/helpers@0.5.15))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)) optionalDependencies: - '@docusaurus/faster': 3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15) + '@docusaurus/faster': 3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16) transitivePeerDependencies: - '@minify-html/node' - '@parcel/css' @@ -16862,15 +17042,15 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/core@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': + '@docusaurus/core@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': dependencies: - '@docusaurus/babel': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/bundler': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/babel': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/bundler': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) '@docusaurus/logger': 3.10.1 - '@docusaurus/mdx-loader': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/mdx-loader': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@mdx-js/react': 3.1.1(@types/react@19.2.17)(react@19.2.7) boxen: 6.2.1 chalk: 4.1.2 @@ -16886,7 +17066,7 @@ snapshots: execa: 5.1.1 fs-extra: 11.2.0 html-tags: 3.3.1 - html-webpack-plugin: 5.6.7(@rspack/core@1.7.11(@swc/helpers@0.5.15))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)) + html-webpack-plugin: 5.6.7(@rspack/core@1.7.11(@swc/helpers@0.5.15))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)) leven: 3.1.0 lodash: 4.18.1 open: 8.4.2 @@ -16896,7 +17076,7 @@ snapshots: react-dom: 19.2.7(react@19.2.7) react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)' react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.7)' - react-loadable-ssr-addon-v5-slorber: 1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.7))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)) + react-loadable-ssr-addon-v5-slorber: 1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.7))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)) react-router: 5.3.4(react@19.2.7) react-router-config: 5.1.1(react-router@5.3.4(react@19.2.7))(react@19.2.7) react-router-dom: 5.3.4(react@19.2.7) @@ -16905,12 +17085,12 @@ snapshots: tinypool: 1.1.1 tslib: 2.8.1 update-notifier: 6.0.2 - webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16) webpack-bundle-analyzer: 4.10.2 - webpack-dev-server: 5.2.4(tslib@2.8.1)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)) + webpack-dev-server: 5.2.4(tslib@2.8.1)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)) webpack-merge: 6.0.1 optionalDependencies: - '@docusaurus/faster': 3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15) + '@docusaurus/faster': 3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16) transitivePeerDependencies: - '@minify-html/node' - '@parcel/css' @@ -16935,23 +17115,23 @@ snapshots: '@docusaurus/cssnano-preset@3.10.1': dependencies: - cssnano-preset-advanced: 6.1.2(postcss@8.5.15) - postcss: 8.5.15 - postcss-sort-media-queries: 5.2.0(postcss@8.5.15) + cssnano-preset-advanced: 6.1.2(postcss@8.5.16) + postcss: 8.5.16 + postcss-sort-media-queries: 5.2.0(postcss@8.5.16) tslib: 2.8.1 - '@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15)': + '@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16)': dependencies: - '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@rspack/core': 1.7.11(@swc/helpers@0.5.15) '@swc/core': 1.15.3(@swc/helpers@0.5.15) '@swc/html': 1.15.40 browserslist: 4.28.2 lightningcss: 1.32.0 semver: 7.7.4 - swc-loader: 0.2.7(@swc/core@1.15.3(@swc/helpers@0.5.15))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)) + swc-loader: 0.2.7(@swc/core@1.15.3(@swc/helpers@0.5.15))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)) tslib: 2.8.1 - webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(@swc/html@1.15.40)(esbuild@0.28.0)(lightningcss@1.32.0)(postcss@8.5.15) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(@swc/html@1.15.40)(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.16) transitivePeerDependencies: - '@minify-html/node' - '@swc/css' @@ -16970,16 +17150,16 @@ snapshots: chalk: 4.1.2 tslib: 2.8.1 - '@docusaurus/mdx-loader@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/mdx-loader@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@docusaurus/logger': 3.10.1 - '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@mdx-js/mdx': 3.1.1 '@slorber/remark-comment': 1.0.0 escape-html: 1.0.3 estree-util-value-to-estree: 3.5.0 - file-loader: 6.2.0(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)) + file-loader: 6.2.0(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)) fs-extra: 11.2.0 image-size: 2.0.2 mdast-util-mdx: 3.0.0 @@ -16995,9 +17175,9 @@ snapshots: tslib: 2.8.1 unified: 11.0.5 unist-util-visit: 5.1.0 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)) vfile: 6.0.3 - webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16) transitivePeerDependencies: - '@minify-html/node' - '@swc/core' @@ -17014,9 +17194,9 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/module-type-aliases@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/module-type-aliases@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@types/history': 4.7.11 '@types/react': 19.2.17 '@types/react-router-config': 5.0.11 @@ -17041,17 +17221,17 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/plugin-content-blog@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': + '@docusaurus/plugin-content-blog@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) '@docusaurus/logger': 3.10.1 - '@docusaurus/mdx-loader': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-content-docs': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) - '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/mdx-loader': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/plugin-content-docs': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) cheerio: 1.0.0-rc.12 combine-promises: 1.2.0 feed: 4.2.2 @@ -17064,7 +17244,7 @@ snapshots: tslib: 2.8.1 unist-util-visit: 5.1.0 utility-types: 3.11.0 - webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -17089,17 +17269,17 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': + '@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) '@docusaurus/logger': 3.10.1 - '@docusaurus/mdx-loader': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/module-type-aliases': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/mdx-loader': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/module-type-aliases': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@types/react-router-config': 5.0.11 combine-promises: 1.2.0 fs-extra: 11.2.0 @@ -17110,7 +17290,7 @@ snapshots: schema-dts: 1.1.5 tslib: 2.8.1 utility-types: 3.11.0 - webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -17135,18 +17315,18 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-pages@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': + '@docusaurus/plugin-content-pages@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) - '@docusaurus/mdx-loader': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/mdx-loader': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) fs-extra: 11.2.0 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) tslib: 2.8.1 - webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -17171,12 +17351,12 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-css-cascade-layers@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': + '@docusaurus/plugin-css-cascade-layers@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) - '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) tslib: 2.8.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -17204,11 +17384,11 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-debug@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': + '@docusaurus/plugin-debug@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) - '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) fs-extra: 11.2.0 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) @@ -17238,11 +17418,11 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-analytics@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': + '@docusaurus/plugin-google-analytics@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) - '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) tslib: 2.8.1 @@ -17270,11 +17450,11 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-gtag@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': + '@docusaurus/plugin-google-gtag@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) - '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@types/gtag.js': 0.0.20 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) @@ -17303,11 +17483,11 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-tag-manager@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': + '@docusaurus/plugin-google-tag-manager@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) - '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) tslib: 2.8.1 @@ -17335,14 +17515,14 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-sitemap@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': + '@docusaurus/plugin-sitemap@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) '@docusaurus/logger': 3.10.1 - '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) fs-extra: 11.2.0 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) @@ -17372,18 +17552,18 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-svgr@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': + '@docusaurus/plugin-svgr@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) - '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@svgr/core': 8.1.0(typescript@6.0.3) '@svgr/webpack': 8.1.0(typescript@6.0.3) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) tslib: 2.8.1 - webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -17408,23 +17588,23 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/preset-classic@3.10.1(@algolia/client-search@5.53.0)(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/react@19.2.17)(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)(typescript@6.0.3)': + '@docusaurus/preset-classic@3.10.1(@algolia/client-search@5.53.0)(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/react@19.2.17)(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) - '@docusaurus/plugin-content-blog': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) - '@docusaurus/plugin-content-docs': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) - '@docusaurus/plugin-content-pages': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) - '@docusaurus/plugin-css-cascade-layers': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) - '@docusaurus/plugin-debug': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) - '@docusaurus/plugin-google-analytics': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) - '@docusaurus/plugin-google-gtag': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) - '@docusaurus/plugin-google-tag-manager': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) - '@docusaurus/plugin-sitemap': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) - '@docusaurus/plugin-svgr': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) - '@docusaurus/theme-classic': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/react@19.2.17)(esbuild@0.28.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) - '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/theme-search-algolia': 3.10.1(@algolia/client-search@5.53.0)(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/react@19.2.17)(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)(typescript@6.0.3) - '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/plugin-content-blog': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/plugin-content-docs': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/plugin-content-pages': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/plugin-css-cascade-layers': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/plugin-debug': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/plugin-google-analytics': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/plugin-google-gtag': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/plugin-google-tag-manager': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/plugin-sitemap': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/plugin-svgr': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/theme-classic': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/theme-search-algolia': 3.10.1(@algolia/client-search@5.53.0)(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/react@19.2.17)(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)(typescript@6.0.3) + '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) transitivePeerDependencies: @@ -17459,28 +17639,28 @@ snapshots: '@types/react': 19.2.17 react: 19.2.7 - '@docusaurus/theme-classic@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/react@19.2.17)(esbuild@0.28.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': + '@docusaurus/theme-classic@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) '@docusaurus/logger': 3.10.1 - '@docusaurus/mdx-loader': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/module-type-aliases': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-content-blog': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) - '@docusaurus/plugin-content-docs': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) - '@docusaurus/plugin-content-pages': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) - '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/mdx-loader': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/module-type-aliases': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/plugin-content-blog': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/plugin-content-docs': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/plugin-content-pages': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@docusaurus/theme-translations': 3.10.1 - '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@mdx-js/react': 3.1.1(@types/react@19.2.17)(react@19.2.7) clsx: 2.1.1 copy-text-to-clipboard: 3.2.2 infima: 0.2.0-alpha.45 lodash: 4.18.1 nprogress: 0.2.0 - postcss: 8.5.15 + postcss: 8.5.16 prism-react-renderer: 2.4.1(react@19.2.7) prismjs: 1.30.0 react: 19.2.7 @@ -17512,13 +17692,13 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/theme-common@3.10.1(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/theme-common@3.10.1(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@docusaurus/mdx-loader': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/module-type-aliases': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-content-docs': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) - '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/mdx-loader': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/module-type-aliases': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/plugin-content-docs': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@types/history': 4.7.11 '@types/react': 19.2.17 '@types/react-router-config': 5.0.11 @@ -17545,13 +17725,13 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/theme-mermaid@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': + '@docusaurus/theme-mermaid@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) - '@docusaurus/module-type-aliases': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/module-type-aliases': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) mermaid: 11.15.0 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) @@ -17581,17 +17761,17 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/theme-search-algolia@3.10.1(@algolia/client-search@5.53.0)(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/react@19.2.17)(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)(typescript@6.0.3)': + '@docusaurus/theme-search-algolia@3.10.1(@algolia/client-search@5.53.0)(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/react@19.2.17)(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)(typescript@6.0.3)': dependencies: '@algolia/autocomplete-core': 1.19.8(@algolia/client-search@5.53.0)(algoliasearch@5.53.0)(search-insights@2.17.3) '@docsearch/react': 4.6.3(@algolia/client-search@5.53.0)(@types/react@19.2.17)(algoliasearch@5.53.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3) - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) '@docusaurus/logger': 3.10.1 - '@docusaurus/plugin-content-docs': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) - '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/plugin-content-docs': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@docusaurus/theme-translations': 3.10.1 - '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) algoliasearch: 5.53.0 algoliasearch-helper: 3.29.1(algoliasearch@5.53.0) clsx: 2.1.1 @@ -17636,7 +17816,7 @@ snapshots: '@docusaurus/tsconfig@3.10.1': {} - '@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@mdx-js/mdx': 3.1.1 '@types/history': 4.7.11 @@ -17648,7 +17828,7 @@ snapshots: react-dom: 19.2.7(react@19.2.7) react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)' utility-types: 3.11.0 - webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16) webpack-merge: 5.10.0 transitivePeerDependencies: - '@minify-html/node' @@ -17666,9 +17846,9 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/utils-common@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/utils-common@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) tslib: 2.8.1 transitivePeerDependencies: - '@minify-html/node' @@ -17688,11 +17868,11 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/utils-validation@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/utils-validation@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@docusaurus/logger': 3.10.1 - '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) fs-extra: 11.2.0 joi: 17.13.3 js-yaml: 4.1.1 @@ -17716,14 +17896,14 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/utils@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/utils@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@docusaurus/logger': 3.10.1 - '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/types': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) escape-string-regexp: 4.0.0 execa: 5.1.1 - file-loader: 6.2.0(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)) + file-loader: 6.2.0(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)) fs-extra: 11.2.0 github-slugger: 1.5.0 globby: 11.1.0 @@ -17736,9 +17916,9 @@ snapshots: prompts: 2.4.2 resolve-pathname: 3.0.0 tslib: 2.8.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)) utility-types: 3.11.0 - webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16) transitivePeerDependencies: - '@minify-html/node' - '@swc/core' @@ -17761,24 +17941,13 @@ snapshots: '@drizzle-team/brocli@0.12.0': {} - '@emnapi/core@1.10.0': - dependencies: - '@emnapi/wasi-threads': 1.2.1 - tslib: 2.8.1 - optional: true - - '@emnapi/core@1.11.0': + '@emnapi/core@1.11.1': dependencies: '@emnapi/wasi-threads': 1.2.2 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.10.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.1': + '@emnapi/runtime@1.11.1': dependencies: tslib: 2.8.1 optional: true @@ -17792,7 +17961,7 @@ snapshots: '@esbuild-kit/core-utils@3.3.2': dependencies: - esbuild: 0.28.0 + esbuild: 0.28.1 source-map-support: 0.5.21 '@esbuild-kit/esm-loader@2.6.5': @@ -17809,6 +17978,9 @@ snapshots: '@esbuild/aix-ppc64@0.28.0': optional: true + '@esbuild/aix-ppc64@0.28.1': + optional: true + '@esbuild/android-arm64@0.25.12': optional: true @@ -17818,6 +17990,9 @@ snapshots: '@esbuild/android-arm64@0.28.0': optional: true + '@esbuild/android-arm64@0.28.1': + optional: true + '@esbuild/android-arm@0.25.12': optional: true @@ -17827,6 +18002,9 @@ snapshots: '@esbuild/android-arm@0.28.0': optional: true + '@esbuild/android-arm@0.28.1': + optional: true + '@esbuild/android-x64@0.25.12': optional: true @@ -17836,6 +18014,9 @@ snapshots: '@esbuild/android-x64@0.28.0': optional: true + '@esbuild/android-x64@0.28.1': + optional: true + '@esbuild/darwin-arm64@0.25.12': optional: true @@ -17845,6 +18026,9 @@ snapshots: '@esbuild/darwin-arm64@0.28.0': optional: true + '@esbuild/darwin-arm64@0.28.1': + optional: true + '@esbuild/darwin-x64@0.25.12': optional: true @@ -17854,6 +18038,9 @@ snapshots: '@esbuild/darwin-x64@0.28.0': optional: true + '@esbuild/darwin-x64@0.28.1': + optional: true + '@esbuild/freebsd-arm64@0.25.12': optional: true @@ -17863,6 +18050,9 @@ snapshots: '@esbuild/freebsd-arm64@0.28.0': optional: true + '@esbuild/freebsd-arm64@0.28.1': + optional: true + '@esbuild/freebsd-x64@0.25.12': optional: true @@ -17872,6 +18062,9 @@ snapshots: '@esbuild/freebsd-x64@0.28.0': optional: true + '@esbuild/freebsd-x64@0.28.1': + optional: true + '@esbuild/linux-arm64@0.25.12': optional: true @@ -17881,6 +18074,9 @@ snapshots: '@esbuild/linux-arm64@0.28.0': optional: true + '@esbuild/linux-arm64@0.28.1': + optional: true + '@esbuild/linux-arm@0.25.12': optional: true @@ -17890,6 +18086,9 @@ snapshots: '@esbuild/linux-arm@0.28.0': optional: true + '@esbuild/linux-arm@0.28.1': + optional: true + '@esbuild/linux-ia32@0.25.12': optional: true @@ -17899,6 +18098,9 @@ snapshots: '@esbuild/linux-ia32@0.28.0': optional: true + '@esbuild/linux-ia32@0.28.1': + optional: true + '@esbuild/linux-loong64@0.25.12': optional: true @@ -17908,6 +18110,9 @@ snapshots: '@esbuild/linux-loong64@0.28.0': optional: true + '@esbuild/linux-loong64@0.28.1': + optional: true + '@esbuild/linux-mips64el@0.25.12': optional: true @@ -17917,6 +18122,9 @@ snapshots: '@esbuild/linux-mips64el@0.28.0': optional: true + '@esbuild/linux-mips64el@0.28.1': + optional: true + '@esbuild/linux-ppc64@0.25.12': optional: true @@ -17926,6 +18134,9 @@ snapshots: '@esbuild/linux-ppc64@0.28.0': optional: true + '@esbuild/linux-ppc64@0.28.1': + optional: true + '@esbuild/linux-riscv64@0.25.12': optional: true @@ -17935,6 +18146,9 @@ snapshots: '@esbuild/linux-riscv64@0.28.0': optional: true + '@esbuild/linux-riscv64@0.28.1': + optional: true + '@esbuild/linux-s390x@0.25.12': optional: true @@ -17944,6 +18158,9 @@ snapshots: '@esbuild/linux-s390x@0.28.0': optional: true + '@esbuild/linux-s390x@0.28.1': + optional: true + '@esbuild/linux-x64@0.25.12': optional: true @@ -17953,6 +18170,9 @@ snapshots: '@esbuild/linux-x64@0.28.0': optional: true + '@esbuild/linux-x64@0.28.1': + optional: true + '@esbuild/netbsd-arm64@0.25.12': optional: true @@ -17962,6 +18182,9 @@ snapshots: '@esbuild/netbsd-arm64@0.28.0': optional: true + '@esbuild/netbsd-arm64@0.28.1': + optional: true + '@esbuild/netbsd-x64@0.25.12': optional: true @@ -17971,6 +18194,9 @@ snapshots: '@esbuild/netbsd-x64@0.28.0': optional: true + '@esbuild/netbsd-x64@0.28.1': + optional: true + '@esbuild/openbsd-arm64@0.25.12': optional: true @@ -17980,6 +18206,9 @@ snapshots: '@esbuild/openbsd-arm64@0.28.0': optional: true + '@esbuild/openbsd-arm64@0.28.1': + optional: true + '@esbuild/openbsd-x64@0.25.12': optional: true @@ -17989,6 +18218,9 @@ snapshots: '@esbuild/openbsd-x64@0.28.0': optional: true + '@esbuild/openbsd-x64@0.28.1': + optional: true + '@esbuild/openharmony-arm64@0.25.12': optional: true @@ -17998,6 +18230,9 @@ snapshots: '@esbuild/openharmony-arm64@0.28.0': optional: true + '@esbuild/openharmony-arm64@0.28.1': + optional: true + '@esbuild/sunos-x64@0.25.12': optional: true @@ -18007,6 +18242,9 @@ snapshots: '@esbuild/sunos-x64@0.28.0': optional: true + '@esbuild/sunos-x64@0.28.1': + optional: true + '@esbuild/win32-arm64@0.25.12': optional: true @@ -18016,6 +18254,9 @@ snapshots: '@esbuild/win32-arm64@0.28.0': optional: true + '@esbuild/win32-arm64@0.28.1': + optional: true + '@esbuild/win32-ia32@0.25.12': optional: true @@ -18025,6 +18266,9 @@ snapshots: '@esbuild/win32-ia32@0.28.0': optional: true + '@esbuild/win32-ia32@0.28.1': + optional: true + '@esbuild/win32-x64@0.25.12': optional: true @@ -18034,6 +18278,9 @@ snapshots: '@esbuild/win32-x64@0.28.0': optional: true + '@esbuild/win32-x64@0.28.1': + optional: true + '@exodus/bytes@1.15.0(@noble/hashes@2.0.1)': optionalDependencies: '@noble/hashes': 2.0.1 @@ -18041,7 +18288,7 @@ snapshots: '@extractus/feed-extractor@7.2.1': dependencies: '@pwshub/bellajs': 13.0.2 - fast-xml-parser: 5.9.3 + fast-xml-parser: 5.8.0 html-entities: 2.6.0 '@fastify/ajv-compiler@4.0.5': @@ -18151,32 +18398,14 @@ snapshots: '@formatjs/fast-memoize': 3.1.0 tslib: 2.8.1 - '@gfazioli/mantine-onboarding-tour@4.0.5(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(@tabler/icons-react@3.44.0(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@gfazioli/mantine-onboarding-tour@4.0.6(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(@tabler/icons-react@3.44.0(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@mantine/core': 9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@mantine/hooks': 9.3.2(react@19.2.7) + '@mantine/core': 9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@mantine/hooks': 9.4.1(react@19.2.7) '@tabler/icons-react': 3.44.0(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@gitbeaker/core@43.8.0': - dependencies: - '@gitbeaker/requester-utils': 43.8.0 - qs: 6.15.2 - xcase: 2.0.1 - - '@gitbeaker/requester-utils@43.8.0': - dependencies: - picomatch-browser: 2.2.6 - qs: 6.15.2 - rate-limiter-flexible: 8.1.0 - xcase: 2.0.1 - - '@gitbeaker/rest@43.8.0': - dependencies: - '@gitbeaker/core': 43.8.0 - '@gitbeaker/requester-utils': 43.8.0 - '@grpc/grpc-js@1.12.5': dependencies: '@grpc/proto-loader': 0.7.13 @@ -18197,9 +18426,9 @@ snapshots: dependencies: '@hapi/hoek': 9.3.0 - '@headlessui/tailwindcss@0.2.2(tailwindcss@4.3.1)': + '@headlessui/tailwindcss@0.2.2(tailwindcss@4.3.2)': dependencies: - tailwindcss: 4.3.1 + tailwindcss: 4.3.2 '@headlessui/vue@1.7.23(vue@3.5.35(typescript@6.0.3))': dependencies: @@ -18208,11 +18437,11 @@ snapshots: '@homarr/gridstack@1.12.0': {} - '@homarr/node-unifi@2.6.0(undici@7.27.2)': + '@homarr/node-unifi@2.6.0(undici@7.28.0)': dependencies: axios: 1.9.0 eventemitter2: 6.4.9 - http-cookie-agent: 6.0.8(tough-cookie@5.1.2)(undici@7.27.2) + http-cookie-agent: 6.0.8(tough-cookie@5.1.2)(undici@7.28.0) tough-cookie: 5.1.2 ws: 8.21.0 transitivePeerDependencies: @@ -18318,7 +18547,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.10.0 + '@emnapi/runtime': 1.11.1 optional: true '@img/sharp-win32-arm64@0.34.5': @@ -18330,7 +18559,7 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true - '@immich/sdk@2.7.5': + '@immich/sdk@3.0.1': dependencies: '@oazapfts/runtime': 1.2.0 @@ -18780,91 +19009,91 @@ snapshots: js-base64: 3.7.8 optional: true - '@mantine/charts@9.3.2(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(recharts@3.8.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1))': + '@mantine/charts@9.4.1(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(recharts@3.9.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1))': dependencies: - '@mantine/core': 9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@mantine/hooks': 9.3.2(react@19.2.7) + '@mantine/core': 9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@mantine/hooks': 9.4.1(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - recharts: 3.8.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1) + recharts: 3.9.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1) - '@mantine/colors-generator@9.3.2(chroma-js@3.2.0)': + '@mantine/colors-generator@9.4.1(chroma-js@3.2.0)': dependencies: chroma-js: 3.2.0 - '@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@floating-ui/react': 0.27.19(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@mantine/hooks': 9.3.2(react@19.2.7) + '@mantine/hooks': 9.4.1(react@19.2.7) clsx: 2.1.1 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) react-number-format: 5.4.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) - type-fest: 5.6.0 + type-fest: 5.7.0 transitivePeerDependencies: - '@types/react' - '@mantine/dates@9.3.2(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@mantine/dates@9.4.1(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@mantine/core': 9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@mantine/hooks': 9.3.2(react@19.2.7) + '@mantine/core': 9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@mantine/hooks': 9.4.1(react@19.2.7) clsx: 2.1.1 dayjs: 1.11.21 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@mantine/dropzone@9.3.2(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@mantine/dropzone@9.4.1(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@mantine/core': 9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@mantine/hooks': 9.3.2(react@19.2.7) + '@mantine/core': 9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@mantine/hooks': 9.4.1(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) react-dropzone: 15.0.0(react@19.2.7) - '@mantine/form@9.3.2(react@19.2.7)': + '@mantine/form@9.4.1(react@19.2.7)': dependencies: '@standard-schema/spec': 1.1.0 fast-deep-equal: 3.1.3 klona: 2.0.6 react: 19.2.7 - '@mantine/hooks@9.3.2(react@19.2.7)': + '@mantine/hooks@9.4.1(react@19.2.7)': dependencies: react: 19.2.7 - '@mantine/modals@9.3.2(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@mantine/modals@9.4.1(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@mantine/core': 9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@mantine/hooks': 9.3.2(react@19.2.7) + '@mantine/core': 9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@mantine/hooks': 9.4.1(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@mantine/notifications@9.3.2(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@mantine/notifications@9.4.1(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@mantine/core': 9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@mantine/hooks': 9.3.2(react@19.2.7) - '@mantine/store': 9.3.2(react@19.2.7) + '@mantine/core': 9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@mantine/hooks': 9.4.1(react@19.2.7) + '@mantine/store': 9.4.1(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) react-transition-group: 4.4.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@mantine/spotlight@9.3.2(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@mantine/spotlight@9.4.1(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@mantine/core': 9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@mantine/hooks': 9.3.2(react@19.2.7) - '@mantine/store': 9.3.2(react@19.2.7) + '@mantine/core': 9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@mantine/hooks': 9.4.1(react@19.2.7) + '@mantine/store': 9.4.1(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@mantine/store@9.3.2(react@19.2.7)': + '@mantine/store@9.4.1(react@19.2.7)': dependencies: react: 19.2.7 - '@mantine/tiptap@9.3.2(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(@tiptap/extension-link@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1))(@tiptap/react@3.27.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@mantine/tiptap@9.4.1(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(@tiptap/extension-link@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1))(@tiptap/react@3.27.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@mantine/core': 9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@mantine/hooks': 9.3.2(react@19.2.7) + '@mantine/core': 9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@mantine/hooks': 9.4.1(react@19.2.7) '@tiptap/extension-link': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1) '@tiptap/react': 3.27.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 @@ -18993,42 +19222,42 @@ snapshots: '@napi-rs/wasm-runtime@1.0.7': dependencies: - '@emnapi/core': 1.11.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.2 + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 optional: true - '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.2 + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 optional: true - '@next/env@16.2.9': {} + '@next/env@16.2.10': {} - '@next/swc-darwin-arm64@16.2.9': + '@next/swc-darwin-arm64@16.2.10': optional: true - '@next/swc-darwin-x64@16.2.9': + '@next/swc-darwin-x64@16.2.10': optional: true - '@next/swc-linux-arm64-gnu@16.2.9': + '@next/swc-linux-arm64-gnu@16.2.10': optional: true - '@next/swc-linux-arm64-musl@16.2.9': + '@next/swc-linux-arm64-musl@16.2.10': optional: true - '@next/swc-linux-x64-gnu@16.2.9': + '@next/swc-linux-x64-gnu@16.2.10': optional: true - '@next/swc-linux-x64-musl@16.2.9': + '@next/swc-linux-x64-musl@16.2.10': optional: true - '@next/swc-win32-arm64-msvc@16.2.9': + '@next/swc-win32-arm64-msvc@16.2.10': optional: true - '@next/swc-win32-x64-msvc@16.2.9': + '@next/swc-win32-x64-msvc@16.2.10': optional: true '@nivo/annotations@0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': @@ -19176,6 +19405,8 @@ snapshots: '@noble/hashes@2.0.1': {} + '@nodable/entities@2.1.0': {} + '@nodable/entities@2.2.0': {} '@nodelib/fs.scandir@2.1.5': @@ -19358,120 +19589,120 @@ snapshots: '@opentelemetry/semantic-conventions@1.41.1': {} - '@oxc-project/types@0.133.0': {} + '@oxc-project/types@0.138.0': {} - '@oxfmt/binding-android-arm-eabi@0.55.0': + '@oxfmt/binding-android-arm-eabi@0.57.0': optional: true - '@oxfmt/binding-android-arm64@0.55.0': + '@oxfmt/binding-android-arm64@0.57.0': optional: true - '@oxfmt/binding-darwin-arm64@0.55.0': + '@oxfmt/binding-darwin-arm64@0.57.0': optional: true - '@oxfmt/binding-darwin-x64@0.55.0': + '@oxfmt/binding-darwin-x64@0.57.0': optional: true - '@oxfmt/binding-freebsd-x64@0.55.0': + '@oxfmt/binding-freebsd-x64@0.57.0': optional: true - '@oxfmt/binding-linux-arm-gnueabihf@0.55.0': + '@oxfmt/binding-linux-arm-gnueabihf@0.57.0': optional: true - '@oxfmt/binding-linux-arm-musleabihf@0.55.0': + '@oxfmt/binding-linux-arm-musleabihf@0.57.0': optional: true - '@oxfmt/binding-linux-arm64-gnu@0.55.0': + '@oxfmt/binding-linux-arm64-gnu@0.57.0': optional: true - '@oxfmt/binding-linux-arm64-musl@0.55.0': + '@oxfmt/binding-linux-arm64-musl@0.57.0': optional: true - '@oxfmt/binding-linux-ppc64-gnu@0.55.0': + '@oxfmt/binding-linux-ppc64-gnu@0.57.0': optional: true - '@oxfmt/binding-linux-riscv64-gnu@0.55.0': + '@oxfmt/binding-linux-riscv64-gnu@0.57.0': optional: true - '@oxfmt/binding-linux-riscv64-musl@0.55.0': + '@oxfmt/binding-linux-riscv64-musl@0.57.0': optional: true - '@oxfmt/binding-linux-s390x-gnu@0.55.0': + '@oxfmt/binding-linux-s390x-gnu@0.57.0': optional: true - '@oxfmt/binding-linux-x64-gnu@0.55.0': + '@oxfmt/binding-linux-x64-gnu@0.57.0': optional: true - '@oxfmt/binding-linux-x64-musl@0.55.0': + '@oxfmt/binding-linux-x64-musl@0.57.0': optional: true - '@oxfmt/binding-openharmony-arm64@0.55.0': + '@oxfmt/binding-openharmony-arm64@0.57.0': optional: true - '@oxfmt/binding-win32-arm64-msvc@0.55.0': + '@oxfmt/binding-win32-arm64-msvc@0.57.0': optional: true - '@oxfmt/binding-win32-ia32-msvc@0.55.0': + '@oxfmt/binding-win32-ia32-msvc@0.57.0': optional: true - '@oxfmt/binding-win32-x64-msvc@0.55.0': + '@oxfmt/binding-win32-x64-msvc@0.57.0': optional: true - '@oxlint/binding-android-arm-eabi@1.70.0': + '@oxlint/binding-android-arm-eabi@1.72.0': optional: true - '@oxlint/binding-android-arm64@1.70.0': + '@oxlint/binding-android-arm64@1.72.0': optional: true - '@oxlint/binding-darwin-arm64@1.70.0': + '@oxlint/binding-darwin-arm64@1.72.0': optional: true - '@oxlint/binding-darwin-x64@1.70.0': + '@oxlint/binding-darwin-x64@1.72.0': optional: true - '@oxlint/binding-freebsd-x64@1.70.0': + '@oxlint/binding-freebsd-x64@1.72.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.70.0': + '@oxlint/binding-linux-arm-gnueabihf@1.72.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.70.0': + '@oxlint/binding-linux-arm-musleabihf@1.72.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.70.0': + '@oxlint/binding-linux-arm64-gnu@1.72.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.70.0': + '@oxlint/binding-linux-arm64-musl@1.72.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.70.0': + '@oxlint/binding-linux-ppc64-gnu@1.72.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.70.0': + '@oxlint/binding-linux-riscv64-gnu@1.72.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.70.0': + '@oxlint/binding-linux-riscv64-musl@1.72.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.70.0': + '@oxlint/binding-linux-s390x-gnu@1.72.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.70.0': + '@oxlint/binding-linux-x64-gnu@1.72.0': optional: true - '@oxlint/binding-linux-x64-musl@1.70.0': + '@oxlint/binding-linux-x64-musl@1.72.0': optional: true - '@oxlint/binding-openharmony-arm64@1.70.0': + '@oxlint/binding-openharmony-arm64@1.72.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.70.0': + '@oxlint/binding-win32-arm64-msvc@1.72.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.70.0': + '@oxlint/binding-win32-ia32-msvc@1.72.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.70.0': + '@oxlint/binding-win32-x64-msvc@1.72.0': optional: true '@panva/hkdf@1.2.1': {} @@ -19642,9 +19873,9 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@playwright/test@1.61.0': + '@playwright/test@1.61.1': dependencies: - playwright: 1.61.0 + playwright: 1.61.1 '@pnpm/config.env-replace@1.1.0': {} @@ -19660,11 +19891,11 @@ snapshots: '@polka/url@1.0.0-next.25': {} - '@posthog/core@1.35.3': + '@posthog/core@1.39.6': dependencies: - '@posthog/types': 1.390.2 + '@posthog/types': 1.392.0 - '@posthog/types@1.390.2': {} + '@posthog/types@1.392.0': {} '@protobufjs/aspromise@1.1.2': {} @@ -19768,53 +19999,53 @@ snapshots: '@codemirror/state': 6.6.0 '@codemirror/view': 6.43.0 - '@rolldown/binding-android-arm64@1.0.3': + '@rolldown/binding-android-arm64@1.1.4': optional: true - '@rolldown/binding-darwin-arm64@1.0.3': + '@rolldown/binding-darwin-arm64@1.1.4': optional: true - '@rolldown/binding-darwin-x64@1.0.3': + '@rolldown/binding-darwin-x64@1.1.4': optional: true - '@rolldown/binding-freebsd-x64@1.0.3': + '@rolldown/binding-freebsd-x64@1.1.4': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + '@rolldown/binding-linux-arm-gnueabihf@1.1.4': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.3': + '@rolldown/binding-linux-arm64-gnu@1.1.4': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.3': + '@rolldown/binding-linux-arm64-musl@1.1.4': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.3': + '@rolldown/binding-linux-ppc64-gnu@1.1.4': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.3': + '@rolldown/binding-linux-s390x-gnu@1.1.4': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.3': + '@rolldown/binding-linux-x64-gnu@1.1.4': optional: true - '@rolldown/binding-linux-x64-musl@1.0.3': + '@rolldown/binding-linux-x64-musl@1.1.4': optional: true - '@rolldown/binding-openharmony-arm64@1.0.3': + '@rolldown/binding-openharmony-arm64@1.1.4': optional: true - '@rolldown/binding-wasm32-wasi@1.0.3': + '@rolldown/binding-wasm32-wasi@1.1.4': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.3': + '@rolldown/binding-win32-arm64-msvc@1.1.4': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.3': + '@rolldown/binding-win32-x64-msvc@1.1.4': optional: true '@rolldown/pluginutils@1.0.1': {} @@ -19879,21 +20110,21 @@ snapshots: '@rvf/set-get@7.0.1': {} - '@scalar/agent-chat@0.12.11(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3)': + '@scalar/agent-chat@0.12.16(nprogress@0.2.0)(tailwindcss@4.3.2)(typescript@6.0.3)(zod@4.4.3)': dependencies: '@ai-sdk/vue': 3.0.33(vue@3.5.35(typescript@6.0.3))(zod@4.4.3) - '@scalar/api-client': 3.10.4(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3) - '@scalar/components': 0.27.2(tailwindcss@4.3.1)(typescript@6.0.3) - '@scalar/helpers': 0.8.2 + '@scalar/api-client': 3.13.1(nprogress@0.2.0)(tailwindcss@4.3.2)(typescript@6.0.3) + '@scalar/components': 0.27.4(tailwindcss@4.3.2)(typescript@6.0.3) + '@scalar/helpers': 0.9.0 '@scalar/icons': 0.7.3(typescript@6.0.3) - '@scalar/json-magic': 0.12.16 + '@scalar/json-magic': 0.12.17 '@scalar/openapi-types': 0.9.1 - '@scalar/schemas': 0.5.0 - '@scalar/themes': 0.16.0 - '@scalar/types': 0.14.0 + '@scalar/schemas': 0.7.1 + '@scalar/themes': 0.16.2 + '@scalar/types': 0.16.1 '@scalar/use-toasts': 0.10.2(typescript@6.0.3) '@scalar/validation': 0.6.0 - '@scalar/workspace-store': 0.54.4(typescript@6.0.3) + '@scalar/workspace-store': 0.55.2(typescript@6.0.3) '@vueuse/core': 13.9.0(vue@3.5.35(typescript@6.0.3)) ai: 6.0.33(zod@4.4.3) js-base64: 3.7.8 @@ -19917,33 +20148,32 @@ snapshots: - universal-cookie - zod - '@scalar/api-client@3.10.4(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)': + '@scalar/api-client@3.13.1(nprogress@0.2.0)(tailwindcss@4.3.2)(typescript@6.0.3)': dependencies: - '@headlessui/tailwindcss': 0.2.2(tailwindcss@4.3.1) + '@headlessui/tailwindcss': 0.2.2(tailwindcss@4.3.2) '@headlessui/vue': 1.7.23(vue@3.5.35(typescript@6.0.3)) - '@scalar/components': 0.27.2(tailwindcss@4.3.1)(typescript@6.0.3) - '@scalar/helpers': 0.8.2 + '@scalar/blocks': 0.1.2(tailwindcss@4.3.2)(typescript@6.0.3) + '@scalar/components': 0.27.4(tailwindcss@4.3.2)(typescript@6.0.3) + '@scalar/helpers': 0.9.0 '@scalar/icons': 0.7.3(typescript@6.0.3) - '@scalar/json-magic': 0.12.16 - '@scalar/oas-utils': 0.18.5(typescript@6.0.3) + '@scalar/oas-utils': 0.19.3(typescript@6.0.3) '@scalar/openapi-types': 0.9.1 - '@scalar/sidebar': 0.9.23(tailwindcss@4.3.1)(typescript@6.0.3) - '@scalar/snippetz': 0.9.17 - '@scalar/themes': 0.16.0 + '@scalar/sidebar': 0.9.27(tailwindcss@4.3.2)(typescript@6.0.3) + '@scalar/snippetz': 0.9.20 + '@scalar/themes': 0.16.2 '@scalar/typebox': 0.1.3 - '@scalar/types': 0.14.0 + '@scalar/types': 0.16.1 '@scalar/use-codemirror': 0.14.12(typescript@6.0.3) '@scalar/use-hooks': 0.4.7(typescript@6.0.3) '@scalar/use-toasts': 0.10.2(typescript@6.0.3) - '@scalar/workspace-store': 0.54.4(typescript@6.0.3) - '@types/har-format': 1.2.16 + '@scalar/workspace-store': 0.55.2(typescript@6.0.3) '@vueuse/core': 13.9.0(vue@3.5.35(typescript@6.0.3)) '@vueuse/integrations': 13.9.0(focus-trap@7.8.0)(fuse.js@7.4.2)(nprogress@0.2.0)(vue@3.5.35(typescript@6.0.3)) focus-trap: 7.8.0 fuse.js: 7.4.2 js-base64: 3.7.8 jsonc-parser: 3.3.1 - nanoid: 5.1.11 + nanoid: 5.1.16 pretty-ms: 9.3.0 radix-vue: 1.9.17(vue@3.5.35(typescript@6.0.3)) set-cookie-parser: 3.1.0 @@ -19966,10 +20196,10 @@ snapshots: - typescript - universal-cookie - '@scalar/api-reference-react@0.9.47(nprogress@0.2.0)(react@19.2.7)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3)': + '@scalar/api-reference-react@0.9.52(nprogress@0.2.0)(react@19.2.7)(tailwindcss@4.3.2)(typescript@6.0.3)(zod@4.4.3)': dependencies: - '@scalar/api-reference': 1.60.0(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3) - '@scalar/types': 0.14.0 + '@scalar/api-reference': 1.62.3(nprogress@0.2.0)(tailwindcss@4.3.2)(typescript@6.0.3)(zod@4.4.3) + '@scalar/types': 0.16.1 react: 19.2.7 transitivePeerDependencies: - '@vue/composition-api' @@ -19988,30 +20218,31 @@ snapshots: - universal-cookie - zod - '@scalar/api-reference@1.60.0(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3)': + '@scalar/api-reference@1.62.3(nprogress@0.2.0)(tailwindcss@4.3.2)(typescript@6.0.3)(zod@4.4.3)': dependencies: '@headlessui/vue': 1.7.23(vue@3.5.35(typescript@6.0.3)) - '@scalar/agent-chat': 0.12.11(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3) - '@scalar/api-client': 3.10.4(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3) - '@scalar/code-highlight': 0.3.6 - '@scalar/components': 0.27.2(tailwindcss@4.3.1)(typescript@6.0.3) - '@scalar/helpers': 0.8.2 + '@scalar/agent-chat': 0.12.16(nprogress@0.2.0)(tailwindcss@4.3.2)(typescript@6.0.3)(zod@4.4.3) + '@scalar/api-client': 3.13.1(nprogress@0.2.0)(tailwindcss@4.3.2)(typescript@6.0.3) + '@scalar/blocks': 0.1.2(tailwindcss@4.3.2)(typescript@6.0.3) + '@scalar/code-highlight': 0.4.0 + '@scalar/components': 0.27.4(tailwindcss@4.3.2)(typescript@6.0.3) + '@scalar/helpers': 0.9.0 '@scalar/icons': 0.7.3(typescript@6.0.3) - '@scalar/oas-utils': 0.18.5(typescript@6.0.3) - '@scalar/schemas': 0.5.0 - '@scalar/sidebar': 0.9.23(tailwindcss@4.3.1)(typescript@6.0.3) - '@scalar/snippetz': 0.9.17 - '@scalar/themes': 0.16.0 - '@scalar/types': 0.14.0 + '@scalar/oas-utils': 0.19.3(typescript@6.0.3) + '@scalar/schemas': 0.7.1 + '@scalar/sidebar': 0.9.27(tailwindcss@4.3.2)(typescript@6.0.3) + '@scalar/snippetz': 0.9.20 + '@scalar/themes': 0.16.2 + '@scalar/types': 0.16.1 '@scalar/use-hooks': 0.4.7(typescript@6.0.3) '@scalar/use-toasts': 0.10.2(typescript@6.0.3) '@scalar/validation': 0.6.0 - '@scalar/workspace-store': 0.54.4(typescript@6.0.3) + '@scalar/workspace-store': 0.55.2(typescript@6.0.3) '@unhead/vue': 2.1.15(vue@3.5.35(typescript@6.0.3)) '@vueuse/core': 13.9.0(vue@3.5.35(typescript@6.0.3)) fuse.js: 7.4.2 microdiff: 1.5.0 - nanoid: 5.1.11 + nanoid: 5.1.16 vue: 3.5.35(typescript@6.0.3) yaml: 2.9.0 transitivePeerDependencies: @@ -20031,7 +20262,29 @@ snapshots: - universal-cookie - zod - '@scalar/code-highlight@0.3.6': + '@scalar/asyncapi-upgrader@0.1.2': + dependencies: + '@scalar/helpers': 0.9.0 + + '@scalar/blocks@0.1.2(tailwindcss@4.3.2)(typescript@6.0.3)': + dependencies: + '@scalar/components': 0.27.4(tailwindcss@4.3.2)(typescript@6.0.3) + '@scalar/helpers': 0.9.0 + '@scalar/icons': 0.7.3(typescript@6.0.3) + '@scalar/snippetz': 0.9.20 + '@scalar/themes': 0.16.2 + '@scalar/types': 0.16.1 + '@scalar/workspace-store': 0.55.2(typescript@6.0.3) + '@types/har-format': 1.2.16 + js-base64: 3.7.8 + vue: 3.5.35(typescript@6.0.3) + transitivePeerDependencies: + - '@vue/composition-api' + - supports-color + - tailwindcss + - typescript + + '@scalar/code-highlight@0.4.0': dependencies: hast-util-to-text: 4.0.2 highlight.js: 11.11.1 @@ -20051,16 +20304,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@scalar/components@0.27.2(tailwindcss@4.3.1)(typescript@6.0.3)': + '@scalar/components@0.27.4(tailwindcss@4.3.2)(typescript@6.0.3)': dependencies: '@floating-ui/utils': 0.2.10 '@floating-ui/vue': 1.1.9(vue@3.5.35(typescript@6.0.3)) - '@headlessui/tailwindcss': 0.2.2(tailwindcss@4.3.1) + '@headlessui/tailwindcss': 0.2.2(tailwindcss@4.3.2) '@headlessui/vue': 1.7.23(vue@3.5.35(typescript@6.0.3)) - '@scalar/code-highlight': 0.3.6 - '@scalar/helpers': 0.8.2 + '@scalar/code-highlight': 0.4.0 + '@scalar/helpers': 0.9.0 '@scalar/icons': 0.7.3(typescript@6.0.3) - '@scalar/themes': 0.16.0 + '@scalar/themes': 0.16.2 '@scalar/use-hooks': 0.4.7(typescript@6.0.3) '@vueuse/core': 13.9.0(vue@3.5.35(typescript@6.0.3)) cva: 1.0.0-beta.4(typescript@6.0.3) @@ -20073,7 +20326,7 @@ snapshots: - tailwindcss - typescript - '@scalar/helpers@0.8.2': {} + '@scalar/helpers@0.9.0': {} '@scalar/icons@0.7.3(typescript@6.0.3)': dependencies: @@ -20084,18 +20337,18 @@ snapshots: transitivePeerDependencies: - typescript - '@scalar/json-magic@0.12.16': + '@scalar/json-magic@0.12.17': dependencies: - '@scalar/helpers': 0.8.2 + '@scalar/helpers': 0.9.0 pathe: 2.0.3 yaml: 2.9.0 - '@scalar/oas-utils@0.18.5(typescript@6.0.3)': + '@scalar/oas-utils@0.19.3(typescript@6.0.3)': dependencies: - '@scalar/helpers': 0.8.2 - '@scalar/themes': 0.16.0 - '@scalar/types': 0.14.0 - '@scalar/workspace-store': 0.54.4(typescript@6.0.3) + '@scalar/helpers': 0.9.0 + '@scalar/themes': 0.16.2 + '@scalar/types': 0.16.1 + '@scalar/workspace-store': 0.55.2(typescript@6.0.3) flatted: 3.4.2 vue: 3.5.35(typescript@6.0.3) yaml: 2.9.0 @@ -20108,19 +20361,19 @@ snapshots: dependencies: '@scalar/openapi-types': 0.9.1 - '@scalar/schemas@0.5.0': + '@scalar/schemas@0.7.1': dependencies: - '@scalar/helpers': 0.8.2 + '@scalar/helpers': 0.9.0 '@scalar/validation': 0.6.0 - '@scalar/sidebar@0.9.23(tailwindcss@4.3.1)(typescript@6.0.3)': + '@scalar/sidebar@0.9.27(tailwindcss@4.3.2)(typescript@6.0.3)': dependencies: - '@scalar/components': 0.27.2(tailwindcss@4.3.1)(typescript@6.0.3) - '@scalar/helpers': 0.8.2 + '@scalar/components': 0.27.4(tailwindcss@4.3.2)(typescript@6.0.3) + '@scalar/helpers': 0.9.0 '@scalar/icons': 0.7.3(typescript@6.0.3) - '@scalar/themes': 0.16.0 + '@scalar/themes': 0.16.2 '@scalar/use-hooks': 0.4.7(typescript@6.0.3) - '@scalar/workspace-store': 0.54.4(typescript@6.0.3) + '@scalar/workspace-store': 0.55.2(typescript@6.0.3) vue: 3.5.35(typescript@6.0.3) transitivePeerDependencies: - '@vue/composition-api' @@ -20128,24 +20381,24 @@ snapshots: - tailwindcss - typescript - '@scalar/snippetz@0.9.17': + '@scalar/snippetz@0.9.20': dependencies: - '@scalar/helpers': 0.8.2 - '@scalar/types': 0.14.0 + '@scalar/helpers': 0.9.0 + '@scalar/types': 0.16.1 js-base64: 3.7.8 stringify-object: 6.0.0 - '@scalar/themes@0.16.0': + '@scalar/themes@0.16.2': dependencies: - nanoid: 5.1.11 + nanoid: 5.1.16 '@scalar/typebox@0.1.3': {} - '@scalar/types@0.14.0': + '@scalar/types@0.16.1': dependencies: - '@scalar/helpers': 0.8.2 - nanoid: 5.1.11 - type-fest: 5.6.0 + '@scalar/helpers': 0.9.0 + nanoid: 5.1.16 + type-fest: 5.7.0 zod: 4.4.3 '@scalar/use-codemirror@0.14.12(typescript@6.0.3)': @@ -20188,18 +20441,19 @@ snapshots: '@scalar/validation@0.6.0': {} - '@scalar/workspace-store@0.54.4(typescript@6.0.3)': + '@scalar/workspace-store@0.55.2(typescript@6.0.3)': dependencies: - '@scalar/helpers': 0.8.2 - '@scalar/json-magic': 0.12.16 + '@scalar/asyncapi-upgrader': 0.1.2 + '@scalar/helpers': 0.9.0 + '@scalar/json-magic': 0.12.17 '@scalar/openapi-upgrader': 0.2.9 - '@scalar/schemas': 0.5.0 - '@scalar/snippetz': 0.9.17 + '@scalar/schemas': 0.7.1 + '@scalar/snippetz': 0.9.20 '@scalar/typebox': 0.1.3 - '@scalar/types': 0.14.0 + '@scalar/types': 0.16.1 '@scalar/validation': 0.6.0 js-base64: 3.7.8 - type-fest: 5.6.0 + type-fest: 5.7.0 vue: 3.5.35(typescript@6.0.3) yaml: 2.9.0 transitivePeerDependencies: @@ -20249,7 +20503,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@semantic-release/github@12.0.8(semantic-release@25.0.5(typescript@6.0.3))': + '@semantic-release/github@12.0.9(semantic-release@25.0.5(typescript@6.0.3))': dependencies: '@octokit/core': 7.0.6 '@octokit/plugin-paginate-rest': 14.0.0(@octokit/core@7.0.6) @@ -20267,7 +20521,7 @@ snapshots: p-filter: 4.1.0 semantic-release: 25.0.5(typescript@6.0.3) tinyglobby: 0.2.17 - undici: 7.27.2 + undici: 7.28.0 url-join: 5.0.0 transitivePeerDependencies: - supports-color @@ -20313,9 +20567,9 @@ snapshots: '@sideway/pinpoint@2.0.0': {} - '@signalwire/docusaurus-plugin-llms-txt@1.2.2(@docusaurus/core@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))': + '@signalwire/docusaurus-plugin-llms-txt@1.2.2(@docusaurus/core@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) fs-extra: 11.2.0 hast-util-select: 6.0.4 hast-util-to-html: 9.0.5 @@ -20592,7 +20846,7 @@ snapshots: '@tabler/icons@3.44.0': {} - '@tailwindcss/node@4.3.1': + '@tailwindcss/node@4.3.2': dependencies: '@jridgewell/remapping': 2.3.5 enhanced-resolve: 5.21.6 @@ -20600,94 +20854,105 @@ snapshots: lightningcss: 1.32.0 magic-string: 0.30.21 source-map-js: 1.2.1 - tailwindcss: 4.3.1 + tailwindcss: 4.3.2 - '@tailwindcss/oxide-android-arm64@4.3.1': + '@tailwindcss/oxide-android-arm64@4.3.2': optional: true - '@tailwindcss/oxide-darwin-arm64@4.3.1': + '@tailwindcss/oxide-darwin-arm64@4.3.2': optional: true - '@tailwindcss/oxide-darwin-x64@4.3.1': + '@tailwindcss/oxide-darwin-x64@4.3.2': optional: true - '@tailwindcss/oxide-freebsd-x64@4.3.1': + '@tailwindcss/oxide-freebsd-x64@4.3.2': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.3.1': + '@tailwindcss/oxide-linux-arm64-gnu@4.3.2': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.3.1': + '@tailwindcss/oxide-linux-arm64-musl@4.3.2': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.3.1': + '@tailwindcss/oxide-linux-x64-gnu@4.3.2': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.3.1': + '@tailwindcss/oxide-linux-x64-musl@4.3.2': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.3.1': + '@tailwindcss/oxide-wasm32-wasi@4.3.2': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.3.1': + '@tailwindcss/oxide-win32-arm64-msvc@4.3.2': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.3.1': + '@tailwindcss/oxide-win32-x64-msvc@4.3.2': optional: true - '@tailwindcss/oxide@4.3.1': + '@tailwindcss/oxide@4.3.2': optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.3.1 - '@tailwindcss/oxide-darwin-arm64': 4.3.1 - '@tailwindcss/oxide-darwin-x64': 4.3.1 - '@tailwindcss/oxide-freebsd-x64': 4.3.1 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.1 - '@tailwindcss/oxide-linux-arm64-gnu': 4.3.1 - '@tailwindcss/oxide-linux-arm64-musl': 4.3.1 - '@tailwindcss/oxide-linux-x64-gnu': 4.3.1 - '@tailwindcss/oxide-linux-x64-musl': 4.3.1 - '@tailwindcss/oxide-wasm32-wasi': 4.3.1 - '@tailwindcss/oxide-win32-arm64-msvc': 4.3.1 - '@tailwindcss/oxide-win32-x64-msvc': 4.3.1 + '@tailwindcss/oxide-android-arm64': 4.3.2 + '@tailwindcss/oxide-darwin-arm64': 4.3.2 + '@tailwindcss/oxide-darwin-x64': 4.3.2 + '@tailwindcss/oxide-freebsd-x64': 4.3.2 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.2 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.2 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.2 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.2 + '@tailwindcss/oxide-linux-x64-musl': 4.3.2 + '@tailwindcss/oxide-wasm32-wasi': 4.3.2 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.2 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.2 - '@tailwindcss/postcss@4.3.1': + '@tailwindcss/postcss@4.3.2': dependencies: '@alloc/quick-lru': 5.2.0 - '@tailwindcss/node': 4.3.1 - '@tailwindcss/oxide': 4.3.1 + '@tailwindcss/node': 4.3.2 + '@tailwindcss/oxide': 4.3.2 postcss: 8.5.15 - tailwindcss: 4.3.1 + tailwindcss: 4.3.2 '@tanstack/match-sorter-utils@8.19.4': dependencies: remove-accents: 0.5.0 - '@tanstack/query-core@5.101.0': {} - - '@tanstack/query-devtools@5.101.0': {} - - '@tanstack/query-persist-client-core@5.101.0': + '@tanstack/query-async-storage-persister@5.101.2': dependencies: - '@tanstack/query-core': 5.101.0 + '@tanstack/query-core': 5.101.2 + '@tanstack/query-persist-client-core': 5.101.2 - '@tanstack/react-query-devtools@5.101.0(@tanstack/react-query@5.101.0(react@19.2.7))(react@19.2.7)': + '@tanstack/query-core@5.101.2': {} + + '@tanstack/query-devtools@5.101.2': {} + + '@tanstack/query-persist-client-core@5.101.2': dependencies: - '@tanstack/query-devtools': 5.101.0 - '@tanstack/react-query': 5.101.0(react@19.2.7) + '@tanstack/query-core': 5.101.2 + + '@tanstack/react-query-devtools@5.101.2(@tanstack/react-query@5.101.2(react@19.2.7))(react@19.2.7)': + dependencies: + '@tanstack/query-devtools': 5.101.2 + '@tanstack/react-query': 5.101.2(react@19.2.7) react: 19.2.7 - '@tanstack/react-query-next-experimental@5.101.0(@tanstack/react-query@5.101.0(react@19.2.7))(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(react@19.2.7)': + '@tanstack/react-query-next-experimental@5.101.2(@tanstack/react-query@5.101.2(react@19.2.7))(next@16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(react@19.2.7)': dependencies: - '@tanstack/react-query': 5.101.0(react@19.2.7) - next: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) + '@tanstack/react-query': 5.101.2(react@19.2.7) + next: 16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) react: 19.2.7 - '@tanstack/react-query@5.101.0(react@19.2.7)': + '@tanstack/react-query-persist-client@5.101.2(@tanstack/react-query@5.101.2(react@19.2.7))(react@19.2.7)': dependencies: - '@tanstack/query-core': 5.101.0 + '@tanstack/query-persist-client-core': 5.101.2 + '@tanstack/react-query': 5.101.2(react@19.2.7) + react: 19.2.7 + + '@tanstack/react-query@5.101.2(react@19.2.7)': + dependencies: + '@tanstack/query-core': 5.101.2 react: 19.2.7 '@tanstack/react-table@8.20.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': @@ -20713,23 +20978,23 @@ snapshots: '@tanstack/virtual-core': 3.17.0 vue: 3.5.35(typescript@6.0.3) - '@testcontainers/mysql@12.0.3': + '@testcontainers/mysql@12.0.4': dependencies: - testcontainers: 12.0.3 + testcontainers: 12.0.4 transitivePeerDependencies: - bare-buffer - supports-color - '@testcontainers/postgresql@12.0.3': + '@testcontainers/postgresql@12.0.4': dependencies: - testcontainers: 12.0.3 + testcontainers: 12.0.4 transitivePeerDependencies: - bare-buffer - supports-color - '@testcontainers/redis@12.0.3': + '@testcontainers/redis@12.0.4': dependencies: - testcontainers: 12.0.3 + testcontainers: 12.0.4 transitivePeerDependencies: - bare-buffer - supports-color @@ -20955,71 +21220,71 @@ snapshots: '@tiptap/extensions': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1) '@tiptap/pm': 3.27.1 - '@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@6.0.3))(typescript@6.0.3)': + '@trpc/client@11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3)': dependencies: - '@trpc/server': 11.17.0(typescript@6.0.3) + '@trpc/server': 11.18.0(typescript@6.0.3) typescript: 6.0.3 - '@trpc/next@11.17.0(@tanstack/react-query@5.101.0(react@19.2.7))(@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/react-query@11.17.0(@tanstack/react-query@5.101.0(react@19.2.7))(@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/server@11.17.0(typescript@6.0.3))(react@19.2.7)(typescript@6.0.3))(@trpc/server@11.17.0(typescript@6.0.3))(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': + '@trpc/next@11.18.0(@tanstack/react-query@5.101.2(react@19.2.7))(@trpc/client@11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/react-query@11.18.0(@tanstack/react-query@5.101.2(react@19.2.7))(@trpc/client@11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/server@11.18.0(typescript@6.0.3))(react@19.2.7)(typescript@6.0.3))(@trpc/server@11.18.0(typescript@6.0.3))(next@16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)': dependencies: - '@trpc/client': 11.17.0(@trpc/server@11.17.0(typescript@6.0.3))(typescript@6.0.3) - '@trpc/server': 11.17.0(typescript@6.0.3) - next: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) + '@trpc/client': 11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3) + '@trpc/server': 11.18.0(typescript@6.0.3) + next: 16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) typescript: 6.0.3 optionalDependencies: - '@tanstack/react-query': 5.101.0(react@19.2.7) - '@trpc/react-query': 11.17.0(@tanstack/react-query@5.101.0(react@19.2.7))(@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/server@11.17.0(typescript@6.0.3))(react@19.2.7)(typescript@6.0.3) + '@tanstack/react-query': 5.101.2(react@19.2.7) + '@trpc/react-query': 11.18.0(@tanstack/react-query@5.101.2(react@19.2.7))(@trpc/client@11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/server@11.18.0(typescript@6.0.3))(react@19.2.7)(typescript@6.0.3) - '@trpc/react-query@11.17.0(@tanstack/react-query@5.101.0(react@19.2.7))(@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/server@11.17.0(typescript@6.0.3))(react@19.2.7)(typescript@6.0.3)': + '@trpc/react-query@11.18.0(@tanstack/react-query@5.101.2(react@19.2.7))(@trpc/client@11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/server@11.18.0(typescript@6.0.3))(react@19.2.7)(typescript@6.0.3)': dependencies: - '@tanstack/react-query': 5.101.0(react@19.2.7) - '@trpc/client': 11.17.0(@trpc/server@11.17.0(typescript@6.0.3))(typescript@6.0.3) - '@trpc/server': 11.17.0(typescript@6.0.3) + '@tanstack/react-query': 5.101.2(react@19.2.7) + '@trpc/client': 11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3) + '@trpc/server': 11.18.0(typescript@6.0.3) react: 19.2.7 typescript: 6.0.3 - '@trpc/server@11.17.0(typescript@6.0.3)': + '@trpc/server@11.18.0(typescript@6.0.3)': dependencies: typescript: 6.0.3 - '@trpc/tanstack-react-query@11.17.0(@tanstack/react-query@5.101.0(react@19.2.7))(@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/server@11.17.0(typescript@6.0.3))(react@19.2.7)(typescript@6.0.3)': + '@trpc/tanstack-react-query@11.18.0(@tanstack/react-query@5.101.2(react@19.2.7))(@trpc/client@11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/server@11.18.0(typescript@6.0.3))(react@19.2.7)(typescript@6.0.3)': dependencies: - '@tanstack/react-query': 5.101.0(react@19.2.7) - '@trpc/client': 11.17.0(@trpc/server@11.17.0(typescript@6.0.3))(typescript@6.0.3) - '@trpc/server': 11.17.0(typescript@6.0.3) + '@tanstack/react-query': 5.101.2(react@19.2.7) + '@trpc/client': 11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3) + '@trpc/server': 11.18.0(typescript@6.0.3) react: 19.2.7 typescript: 6.0.3 '@tsconfig/docusaurus@2.0.9': {} - '@turbo/darwin-64@2.9.18': + '@turbo/darwin-64@2.10.2': optional: true - '@turbo/darwin-arm64@2.9.18': + '@turbo/darwin-arm64@2.10.2': optional: true - '@turbo/gen@2.9.18(@types/node@24.13.2)': + '@turbo/gen@2.10.2(@types/node@24.13.2)': dependencies: '@inquirer/prompts': 7.10.1(@types/node@24.13.2) - esbuild: 0.25.12 + esbuild: 0.28.1 transitivePeerDependencies: - '@types/node' - '@turbo/linux-64@2.9.18': + '@turbo/linux-64@2.10.2': optional: true - '@turbo/linux-arm64@2.9.18': + '@turbo/linux-arm64@2.10.2': optional: true - '@turbo/windows-64@2.9.18': + '@turbo/windows-64@2.10.2': optional: true - '@turbo/windows-arm64@2.9.18': + '@turbo/windows-arm64@2.10.2': optional: true - '@tybys/wasm-util@0.10.2': + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 optional: true @@ -21480,10 +21745,10 @@ snapshots: global: 4.4.0 is-function: 1.0.2 - '@vitejs/plugin-react@6.0.2(babel-plugin-react-compiler@1.0.0)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.15))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0))': + '@vitejs/plugin-react@6.0.3(babel-plugin-react-compiler@1.0.0)(vite@8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.15))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0) + vite: 8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0) optionalDependencies: babel-plugin-react-compiler: 1.0.0 @@ -21499,7 +21764,7 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@24.13.2)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.15))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)) + vitest: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@24.13.2)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)) '@vitest/expect@4.1.9': dependencies: @@ -21510,14 +21775,14 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.9(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.15))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0))': + '@vitest/mocker@4.1.9(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.9 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.14.6(@types/node@24.13.2)(typescript@6.0.3) - vite: 8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.15))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0) + vite: 8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0) '@vitest/pretty-format@4.1.9': dependencies: @@ -21546,7 +21811,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vitest: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@24.13.2)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.15))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)) + vitest: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@24.13.2)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)) '@vitest/utils@4.1.9': dependencies: @@ -21576,7 +21841,7 @@ snapshots: '@vue/shared': 3.5.35 estree-walker: 2.0.2 magic-string: 0.30.21 - postcss: 8.5.15 + postcss: 8.5.16 source-map-js: 1.2.1 '@vue/compiler-ssr@3.5.35': @@ -21772,7 +22037,7 @@ snapshots: address@1.2.2: {} - adm-zip@0.5.17: {} + adm-zip@0.5.18: {} aes-decrypter@4.0.2: dependencies: @@ -21965,13 +22230,13 @@ snapshots: attr-accept@2.2.5: {} - autoprefixer@10.5.0(postcss@8.5.15): + autoprefixer@10.5.0(postcss@8.5.16): dependencies: browserslist: 4.28.2 caniuse-lite: 1.0.30001793 fraction.js: 5.3.4 picocolors: 1.1.1 - postcss: 8.5.15 + postcss: 8.5.16 postcss-value-parser: 4.2.0 avvio@9.2.0: @@ -21999,12 +22264,12 @@ snapshots: b4a@1.6.6: {} - babel-loader@9.2.1(@babel/core@7.29.0)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)): + babel-loader@9.2.1(@babel/core@7.29.0)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)): dependencies: '@babel/core': 7.29.0 find-cache-dir: 4.0.0 schema-utils: 4.3.3 - webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16) babel-plugin-dynamic-import-node@2.3.3: dependencies: @@ -22079,8 +22344,6 @@ snapshots: bare-events: 2.5.4 optional: true - base-64@1.0.0: {} - base64-js@1.5.1: {} baseline-browser-mapping@2.10.31: {} @@ -22100,10 +22363,10 @@ snapshots: before-after-hook@4.0.0: {} - better-auth@1.6.15(@opentelemetry/api@1.9.0)(better-sqlite3@12.10.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1))(mysql2@3.22.5(@types/node@24.13.2))(next@16.2.9(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(pg@8.22.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)(vue@3.5.35(typescript@6.0.3)): + better-auth@1.6.15(@opentelemetry/api@1.9.0)(better-sqlite3@12.11.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1))(mysql2@3.22.5(@types/node@24.13.2))(next@16.2.10(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(pg@8.22.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)(vue@3.5.35(typescript@6.0.3)): dependencies: '@better-auth/core': 1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) - '@better-auth/drizzle-adapter': 1.6.15(@better-auth/core@1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1)) + '@better-auth/drizzle-adapter': 1.6.15(@better-auth/core@1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1)) '@better-auth/kysely-adapter': 1.6.15(@better-auth/core@1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)(kysely@0.29.2) '@better-auth/memory-adapter': 1.6.15(@better-auth/core@1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1) '@better-auth/mongo-adapter': 1.6.15(@better-auth/core@1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1) @@ -22120,15 +22383,15 @@ snapshots: nanostores: 1.3.0 zod: 4.4.3 optionalDependencies: - better-sqlite3: 12.10.0 + better-sqlite3: 12.11.1 drizzle-kit: 0.31.10 - drizzle-orm: 0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1) + drizzle-orm: 0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1) mysql2: 3.22.5(@types/node@24.13.2) - next: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) + next: 16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) pg: 8.22.0 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - vitest: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@24.13.2)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.15))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)) + vitest: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@24.13.2)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)) vue: 3.5.35(typescript@6.0.3) transitivePeerDependencies: - '@cloudflare/workers-types' @@ -22143,7 +22406,7 @@ snapshots: optionalDependencies: zod: 4.4.3 - better-sqlite3@12.10.0: + better-sqlite3@12.11.1: dependencies: bindings: 1.5.0 prebuild-install: 7.1.2 @@ -22240,6 +22503,10 @@ snapshots: dependencies: balanced-match: 4.0.2 + brace-expansion@5.0.7: + dependencies: + balanced-match: 4.0.2 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -22613,9 +22880,9 @@ snapshots: dependencies: compare-func: 2.0.0 - conventional-changelog-conventionalcommits@9.3.1: + conventional-changelog-conventionalcommits@10.2.0: dependencies: - compare-func: 2.0.0 + '@conventional-changelog/template': 1.2.0 conventional-changelog-writer@8.0.0: dependencies: @@ -22647,6 +22914,8 @@ snapshots: cookie@1.1.1: {} + cookie@2.0.1: {} + cookies@0.9.1: dependencies: depd: 2.0.0 @@ -22658,7 +22927,7 @@ snapshots: copy-text-to-clipboard@3.2.2: {} - copy-webpack-plugin@11.0.0(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)): + copy-webpack-plugin@11.0.0(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)): dependencies: fast-glob: 3.3.3 glob-parent: 6.0.2 @@ -22666,7 +22935,7 @@ snapshots: normalize-path: 3.0.0 schema-utils: 4.3.3 serialize-javascript: 6.0.2 - webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16) core-js-compat@3.49.0: dependencies: @@ -22745,52 +23014,52 @@ snapshots: dependencies: type-fest: 1.4.0 - css-blank-pseudo@7.0.1(postcss@8.5.15): + css-blank-pseudo@7.0.1(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-selector-parser: 7.1.0 - css-declaration-sorter@7.4.0(postcss@8.5.15): + css-declaration-sorter@7.4.0(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 - css-has-pseudo@7.0.3(postcss@8.5.15): + css-has-pseudo@7.0.3(postcss@8.5.16): dependencies: '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0) - postcss: 8.5.15 + postcss: 8.5.16 postcss-selector-parser: 7.1.0 postcss-value-parser: 4.2.0 - css-loader@6.11.0(@rspack/core@1.7.11(@swc/helpers@0.5.15))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)): + css-loader@6.11.0(@rspack/core@1.7.11(@swc/helpers@0.5.15))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)): dependencies: - icss-utils: 5.1.0(postcss@8.5.15) - postcss: 8.5.15 - postcss-modules-extract-imports: 3.1.0(postcss@8.5.15) - postcss-modules-local-by-default: 4.2.0(postcss@8.5.15) - postcss-modules-scope: 3.2.1(postcss@8.5.15) - postcss-modules-values: 4.0.0(postcss@8.5.15) + icss-utils: 5.1.0(postcss@8.5.16) + postcss: 8.5.16 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.16) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.16) + postcss-modules-scope: 3.2.1(postcss@8.5.16) + postcss-modules-values: 4.0.0(postcss@8.5.16) postcss-value-parser: 4.2.0 semver: 7.7.4 optionalDependencies: '@rspack/core': 1.7.11(@swc/helpers@0.5.15) - webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16) - css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(esbuild@0.28.0)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)): + css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(esbuild@0.28.0)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)): dependencies: '@jridgewell/trace-mapping': 0.3.31 - cssnano: 6.1.2(postcss@8.5.15) + cssnano: 6.1.2(postcss@8.5.16) jest-worker: 29.7.0 - postcss: 8.5.15 + postcss: 8.5.16 schema-utils: 4.3.3 serialize-javascript: 6.0.2 - webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16) optionalDependencies: clean-css: 5.3.3 esbuild: 0.28.0 - css-prefers-color-scheme@10.0.0(postcss@8.5.15): + css-prefers-color-scheme@10.0.0(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 css-select@4.3.0: dependencies: @@ -22831,60 +23100,60 @@ snapshots: cssesc@3.0.0: {} - cssnano-preset-advanced@6.1.2(postcss@8.5.15): + cssnano-preset-advanced@6.1.2(postcss@8.5.16): dependencies: - autoprefixer: 10.5.0(postcss@8.5.15) + autoprefixer: 10.5.0(postcss@8.5.16) browserslist: 4.28.2 - cssnano-preset-default: 6.1.2(postcss@8.5.15) - postcss: 8.5.15 - postcss-discard-unused: 6.0.5(postcss@8.5.15) - postcss-merge-idents: 6.0.3(postcss@8.5.15) - postcss-reduce-idents: 6.0.3(postcss@8.5.15) - postcss-zindex: 6.0.2(postcss@8.5.15) + cssnano-preset-default: 6.1.2(postcss@8.5.16) + postcss: 8.5.16 + postcss-discard-unused: 6.0.5(postcss@8.5.16) + postcss-merge-idents: 6.0.3(postcss@8.5.16) + postcss-reduce-idents: 6.0.3(postcss@8.5.16) + postcss-zindex: 6.0.2(postcss@8.5.16) - cssnano-preset-default@6.1.2(postcss@8.5.15): + cssnano-preset-default@6.1.2(postcss@8.5.16): dependencies: browserslist: 4.28.2 - css-declaration-sorter: 7.4.0(postcss@8.5.15) - cssnano-utils: 4.0.2(postcss@8.5.15) - postcss: 8.5.15 - postcss-calc: 9.0.1(postcss@8.5.15) - postcss-colormin: 6.1.0(postcss@8.5.15) - postcss-convert-values: 6.1.0(postcss@8.5.15) - postcss-discard-comments: 6.0.2(postcss@8.5.15) - postcss-discard-duplicates: 6.0.3(postcss@8.5.15) - postcss-discard-empty: 6.0.3(postcss@8.5.15) - postcss-discard-overridden: 6.0.2(postcss@8.5.15) - postcss-merge-longhand: 6.0.5(postcss@8.5.15) - postcss-merge-rules: 6.1.1(postcss@8.5.15) - postcss-minify-font-values: 6.1.0(postcss@8.5.15) - postcss-minify-gradients: 6.0.3(postcss@8.5.15) - postcss-minify-params: 6.1.0(postcss@8.5.15) - postcss-minify-selectors: 6.0.4(postcss@8.5.15) - postcss-normalize-charset: 6.0.2(postcss@8.5.15) - postcss-normalize-display-values: 6.0.2(postcss@8.5.15) - postcss-normalize-positions: 6.0.2(postcss@8.5.15) - postcss-normalize-repeat-style: 6.0.2(postcss@8.5.15) - postcss-normalize-string: 6.0.2(postcss@8.5.15) - postcss-normalize-timing-functions: 6.0.2(postcss@8.5.15) - postcss-normalize-unicode: 6.1.0(postcss@8.5.15) - postcss-normalize-url: 6.0.2(postcss@8.5.15) - postcss-normalize-whitespace: 6.0.2(postcss@8.5.15) - postcss-ordered-values: 6.0.2(postcss@8.5.15) - postcss-reduce-initial: 6.1.0(postcss@8.5.15) - postcss-reduce-transforms: 6.0.2(postcss@8.5.15) - postcss-svgo: 6.0.3(postcss@8.5.15) - postcss-unique-selectors: 6.0.4(postcss@8.5.15) + css-declaration-sorter: 7.4.0(postcss@8.5.16) + cssnano-utils: 4.0.2(postcss@8.5.16) + postcss: 8.5.16 + postcss-calc: 9.0.1(postcss@8.5.16) + postcss-colormin: 6.1.0(postcss@8.5.16) + postcss-convert-values: 6.1.0(postcss@8.5.16) + postcss-discard-comments: 6.0.2(postcss@8.5.16) + postcss-discard-duplicates: 6.0.3(postcss@8.5.16) + postcss-discard-empty: 6.0.3(postcss@8.5.16) + postcss-discard-overridden: 6.0.2(postcss@8.5.16) + postcss-merge-longhand: 6.0.5(postcss@8.5.16) + postcss-merge-rules: 6.1.1(postcss@8.5.16) + postcss-minify-font-values: 6.1.0(postcss@8.5.16) + postcss-minify-gradients: 6.0.3(postcss@8.5.16) + postcss-minify-params: 6.1.0(postcss@8.5.16) + postcss-minify-selectors: 6.0.4(postcss@8.5.16) + postcss-normalize-charset: 6.0.2(postcss@8.5.16) + postcss-normalize-display-values: 6.0.2(postcss@8.5.16) + postcss-normalize-positions: 6.0.2(postcss@8.5.16) + postcss-normalize-repeat-style: 6.0.2(postcss@8.5.16) + postcss-normalize-string: 6.0.2(postcss@8.5.16) + postcss-normalize-timing-functions: 6.0.2(postcss@8.5.16) + postcss-normalize-unicode: 6.1.0(postcss@8.5.16) + postcss-normalize-url: 6.0.2(postcss@8.5.16) + postcss-normalize-whitespace: 6.0.2(postcss@8.5.16) + postcss-ordered-values: 6.0.2(postcss@8.5.16) + postcss-reduce-initial: 6.1.0(postcss@8.5.16) + postcss-reduce-transforms: 6.0.2(postcss@8.5.16) + postcss-svgo: 6.0.3(postcss@8.5.16) + postcss-unique-selectors: 6.0.4(postcss@8.5.16) - cssnano-utils@4.0.2(postcss@8.5.15): + cssnano-utils@4.0.2(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 - cssnano@6.1.2(postcss@8.5.15): + cssnano@6.1.2(postcss@8.5.16): dependencies: - cssnano-preset-default: 6.1.2(postcss@8.5.15) + cssnano-preset-default: 6.1.2(postcss@8.5.16) lilconfig: 3.1.3 - postcss: 8.5.15 + postcss: 8.5.16 csso@5.0.5: dependencies: @@ -23232,9 +23501,20 @@ snapshots: transitivePeerDependencies: - supports-color - docusaurus-plugin-image-zoom@3.0.1(@docusaurus/theme-classic@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/react@19.2.17)(esbuild@0.28.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)): + dockerode@5.0.1: dependencies: - '@docusaurus/theme-classic': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.0)(postcss@8.5.15))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/react@19.2.17)(esbuild@0.28.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + '@balena/dockerignore': 1.0.2 + '@grpc/grpc-js': 1.12.5 + '@grpc/proto-loader': 0.7.13 + docker-modem: 5.0.7 + protobufjs: 7.4.0 + tar-fs: 2.1.4 + transitivePeerDependencies: + - supports-color + + docusaurus-plugin-image-zoom@3.0.1(@docusaurus/theme-classic@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)): + dependencies: + '@docusaurus/theme-classic': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) medium-zoom: 1.1.0 validate-peer-dependencies: 2.2.0 @@ -23326,22 +23606,22 @@ snapshots: esbuild: 0.25.12 tsx: 4.21.0 - drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1): + drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1): optionalDependencies: '@libsql/client-wasm': 0.14.0 '@opentelemetry/api': 1.9.0 '@types/better-sqlite3': 7.6.13 '@types/pg': 8.20.0 - better-sqlite3: 12.10.0 + better-sqlite3: 12.11.1 gel: 2.0.0 kysely: 0.29.2 mysql2: 3.22.5(@types/node@24.13.2) pg: 8.22.0 sql.js: 1.14.1 - drizzle-zod@0.8.3(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1))(zod@4.4.3): + drizzle-zod@0.8.3(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1))(zod@4.4.3): dependencies: - drizzle-orm: 0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1) + drizzle-orm: 0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1) zod: 4.4.3 dunder-proto@1.0.1: @@ -23545,6 +23825,35 @@ snapshots: '@esbuild/win32-ia32': 0.28.0 '@esbuild/win32-x64': 0.28.0 + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + escalade@3.2.0: {} escape-goat@4.0.0: {} @@ -23818,6 +24127,14 @@ snapshots: path-expression-matcher: 1.5.0 xml-naming: 0.1.0 + fast-xml-parser@5.8.0: + dependencies: + '@nodable/entities': 2.1.0 + fast-xml-builder: 1.2.0 + path-expression-matcher: 1.5.0 + strnum: 2.3.0 + xml-naming: 0.1.0 + fast-xml-parser@5.9.3: dependencies: '@nodable/entities': 2.2.0 @@ -23827,7 +24144,7 @@ snapshots: strnum: 2.4.1 xml-naming: 0.1.0 - fastify@5.8.5: + fastify@5.9.0: dependencies: '@fastify/ajv-compiler': 4.0.5 '@fastify/error': 4.2.0 @@ -23836,7 +24153,7 @@ snapshots: abstract-logging: 2.0.1 avvio: 9.2.0 fast-json-stringify: 6.3.0 - find-my-way: 9.5.0 + find-my-way: 9.6.0 light-my-request: 6.6.0 pino: 10.3.1 process-warning: 5.0.0 @@ -23879,11 +24196,11 @@ snapshots: dependencies: is-unicode-supported: 2.1.0 - file-loader@6.2.0(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)): + file-loader@6.2.0(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16) file-selector@2.1.2: dependencies: @@ -23923,7 +24240,7 @@ snapshots: common-path-prefix: 3.0.0 pkg-dir: 7.0.0 - find-my-way@9.5.0: + find-my-way@9.6.0: dependencies: fast-deep-equal: 3.1.3 fast-querystring: 1.1.2 @@ -24049,7 +24366,7 @@ snapshots: get-own-enumerable-property-symbols@3.0.2: {} - get-port@7.2.0: {} + get-port@5.1.1: {} get-proto@1.0.1: dependencies: @@ -24516,7 +24833,7 @@ snapshots: html-void-elements@3.0.0: {} - html-webpack-plugin@5.6.7(@rspack/core@1.7.11(@swc/helpers@0.5.15))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)): + html-webpack-plugin@5.6.7(@rspack/core@1.7.11(@swc/helpers@0.5.15))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -24525,7 +24842,7 @@ snapshots: tapable: 2.3.3 optionalDependencies: '@rspack/core': 1.7.11(@swc/helpers@0.5.15) - webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16) html-whitespace-sensitive-tag-names@3.0.1: {} @@ -24552,12 +24869,12 @@ snapshots: http-cache-semantics@4.2.0: {} - http-cookie-agent@6.0.8(tough-cookie@5.1.2)(undici@7.27.2): + http-cookie-agent@6.0.8(tough-cookie@5.1.2)(undici@7.28.0): dependencies: agent-base: 7.1.3 tough-cookie: 5.1.2 optionalDependencies: - undici: 7.27.2 + undici: 7.28.0 http-deceiver@1.2.7: {} @@ -24640,11 +24957,11 @@ snapshots: dependencies: safer-buffer: 2.1.2 - icss-utils@5.1.0(postcss@8.5.15): + icss-utils@5.1.0(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 - icu-minify@4.13.0: + icu-minify@4.13.1: dependencies: '@formatjs/icu-messageformat-parser': 3.5.0 @@ -24660,10 +24977,10 @@ snapshots: immediate@3.0.6: {} - immer@10.2.0: {} - immer@11.1.4: {} + immer@11.1.9: {} + immutable@5.1.5: {} import-fresh@3.3.0: @@ -24993,7 +25310,7 @@ snapshots: saxes: 6.0.0 symbol-tree: 3.2.4 tough-cookie: 6.0.1 - undici: 7.27.2 + undici: 7.28.0 w3c-xmlserializer: 5.0.0 webidl-conversions: 8.0.1 whatwg-mimetype: 5.0.0 @@ -25256,7 +25573,7 @@ snapshots: lru.min@1.1.4: {} - lucide-react@1.21.0(react@19.2.7): + lucide-react@1.23.0(react@19.2.7): dependencies: react: 19.2.7 @@ -25280,19 +25597,19 @@ snapshots: dependencies: semver: 7.7.4 - mantine-datatable@9.3.1(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(clsx@2.1.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + mantine-datatable@9.3.1(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(clsx@2.1.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - '@mantine/core': 9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@mantine/hooks': 9.3.2(react@19.2.7) + '@mantine/core': 9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@mantine/hooks': 9.4.1(react@19.2.7) clsx: 2.1.1 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - mantine-react-table-open@9.0.3(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/dates@9.3.2(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(@tabler/icons-react@3.44.0(react@19.2.7))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + mantine-react-table-open@9.0.3(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/dates@9.4.1(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(@tabler/icons-react@3.44.0(react@19.2.7))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - '@mantine/core': 9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@mantine/dates': 9.3.2(@mantine/core@9.3.2(@mantine/hooks@9.3.2(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.3.2(react@19.2.7))(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@mantine/hooks': 9.3.2(react@19.2.7) + '@mantine/core': 9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@mantine/dates': 9.4.1(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@mantine/hooks': 9.4.1(react@19.2.7) '@tabler/icons-react': 3.44.0(react@19.2.7) '@tanstack/match-sorter-utils': 8.19.4 '@tanstack/react-table': 8.20.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -25325,23 +25642,23 @@ snapshots: math-intrinsics@1.1.0: {} - mcp-handler@1.1.0(@modelcontextprotocol/sdk@1.26.0(zod@4.4.3))(next@16.2.9(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)): + mcp-handler@1.1.0(@modelcontextprotocol/sdk@1.26.0(zod@4.4.3))(next@16.2.10(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)): dependencies: '@modelcontextprotocol/sdk': 1.26.0(zod@4.4.3) chalk: 5.6.2 commander: 11.1.0 redis: 4.7.1 optionalDependencies: - next: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) + next: 16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) - mcp-handler@1.1.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)): + mcp-handler@1.1.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(next@16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)): dependencies: '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) chalk: 5.6.2 commander: 11.1.0 redis: 4.7.1 optionalDependencies: - next: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) + next: 16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) mdast-util-directive@3.1.0: dependencies: @@ -25938,11 +26255,11 @@ snapshots: dependencies: dom-walk: 0.1.2 - mini-css-extract-plugin@2.10.2(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)): + mini-css-extract-plugin@2.10.2(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)): dependencies: schema-utils: 4.3.3 tapable: 2.3.3 - webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16) minimalistic-assert@1.0.1: {} @@ -25952,11 +26269,11 @@ snapshots: minimatch@3.1.5: dependencies: - brace-expansion: 5.0.6 + brace-expansion: 5.0.7 minimatch@5.1.9: dependencies: - brace-expansion: 5.0.6 + brace-expansion: 5.0.7 minimatch@9.0.9: dependencies: @@ -26000,7 +26317,7 @@ snapshots: statuses: 2.0.2 strict-event-emitter: 0.5.1 tough-cookie: 6.0.1 - type-fest: 5.6.0 + type-fest: 5.7.0 until-async: 3.0.2 yargs: 17.7.2 optionalDependencies: @@ -26051,9 +26368,7 @@ snapshots: nanoid@3.3.12: {} - nanoid@5.1.11: {} - - nanoid@5.1.15: {} + nanoid@5.1.16: {} nanostores@1.3.0: {} @@ -26071,34 +26386,34 @@ snapshots: neverpanic@0.0.8: {} - next-auth@5.0.0-beta.31(next@16.2.9(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(react@19.2.7): + next-auth@5.0.0-beta.31(next@16.2.10(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(react@19.2.7): dependencies: '@auth/core': 0.41.2 - next: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) + next: 16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) react: 19.2.7 - next-intl-swc-plugin-extractor@4.13.0: {} + next-intl-swc-plugin-extractor@4.13.1: {} - next-intl@4.13.0(next@16.2.9(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(react@19.2.7)(typescript@6.0.3): + next-intl@4.13.1(next@16.2.10(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(react@19.2.7)(typescript@6.0.3): dependencies: '@formatjs/intl-localematcher': 0.8.1 '@parcel/watcher': 2.4.1 '@swc/core': 1.15.3(@swc/helpers@0.5.15) - icu-minify: 4.13.0 + icu-minify: 4.13.1 negotiator: 1.0.0 - next: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) - next-intl-swc-plugin-extractor: 4.13.0 + next: 16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) + next-intl-swc-plugin-extractor: 4.13.1 po-parser: 2.1.1 react: 19.2.7 - use-intl: 4.13.0(react@19.2.7) + use-intl: 4.13.1(react@19.2.7) optionalDependencies: typescript: 6.0.3 transitivePeerDependencies: - '@swc/helpers' - next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0): + next@16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0): dependencies: - '@next/env': 16.2.9 + '@next/env': 16.2.10 '@swc/helpers': 0.5.15 baseline-browser-mapping: 2.10.31 caniuse-lite: 1.0.30001793 @@ -26107,16 +26422,16 @@ snapshots: react-dom: 19.2.7(react@19.2.7) styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.7) optionalDependencies: - '@next/swc-darwin-arm64': 16.2.9 - '@next/swc-darwin-x64': 16.2.9 - '@next/swc-linux-arm64-gnu': 16.2.9 - '@next/swc-linux-arm64-musl': 16.2.9 - '@next/swc-linux-x64-gnu': 16.2.9 - '@next/swc-linux-x64-musl': 16.2.9 - '@next/swc-win32-arm64-msvc': 16.2.9 - '@next/swc-win32-x64-msvc': 16.2.9 + '@next/swc-darwin-arm64': 16.2.10 + '@next/swc-darwin-x64': 16.2.10 + '@next/swc-linux-arm64-gnu': 16.2.10 + '@next/swc-linux-arm64-musl': 16.2.10 + '@next/swc-linux-x64-gnu': 16.2.10 + '@next/swc-linux-x64-musl': 16.2.10 + '@next/swc-win32-arm64-msvc': 16.2.10 + '@next/swc-win32-x64-msvc': 16.2.10 '@opentelemetry/api': 1.9.0 - '@playwright/test': 1.61.0 + '@playwright/test': 1.61.1 babel-plugin-react-compiler: 1.0.0 sass: 1.101.0 sharp: 0.34.5 @@ -26152,10 +26467,10 @@ snapshots: node-gyp-build@4.8.4: {} - node-loader@2.1.0(webpack@5.107.1(esbuild@0.28.0)(postcss@8.5.15)): + node-loader@2.1.0(webpack@5.107.1(esbuild@0.28.1)(postcss@8.5.16)): dependencies: loader-utils: 2.0.4 - webpack: 5.107.1(esbuild@0.28.0)(postcss@8.5.15) + webpack: 5.107.1(esbuild@0.28.1)(postcss@8.5.16) node-mock-http@1.0.4: {} @@ -26200,11 +26515,11 @@ snapshots: dependencies: boolbase: 1.0.0 - null-loader@4.0.1(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)): + null-loader@4.0.1(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16) oauth4webapi@3.3.0: {} @@ -26300,51 +26615,51 @@ snapshots: outvariant@1.4.3: optional: true - oxfmt@0.55.0: + oxfmt@0.57.0: dependencies: tinypool: 2.1.0 optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.55.0 - '@oxfmt/binding-android-arm64': 0.55.0 - '@oxfmt/binding-darwin-arm64': 0.55.0 - '@oxfmt/binding-darwin-x64': 0.55.0 - '@oxfmt/binding-freebsd-x64': 0.55.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.55.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.55.0 - '@oxfmt/binding-linux-arm64-gnu': 0.55.0 - '@oxfmt/binding-linux-arm64-musl': 0.55.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.55.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.55.0 - '@oxfmt/binding-linux-riscv64-musl': 0.55.0 - '@oxfmt/binding-linux-s390x-gnu': 0.55.0 - '@oxfmt/binding-linux-x64-gnu': 0.55.0 - '@oxfmt/binding-linux-x64-musl': 0.55.0 - '@oxfmt/binding-openharmony-arm64': 0.55.0 - '@oxfmt/binding-win32-arm64-msvc': 0.55.0 - '@oxfmt/binding-win32-ia32-msvc': 0.55.0 - '@oxfmt/binding-win32-x64-msvc': 0.55.0 + '@oxfmt/binding-android-arm-eabi': 0.57.0 + '@oxfmt/binding-android-arm64': 0.57.0 + '@oxfmt/binding-darwin-arm64': 0.57.0 + '@oxfmt/binding-darwin-x64': 0.57.0 + '@oxfmt/binding-freebsd-x64': 0.57.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.57.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.57.0 + '@oxfmt/binding-linux-arm64-gnu': 0.57.0 + '@oxfmt/binding-linux-arm64-musl': 0.57.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.57.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.57.0 + '@oxfmt/binding-linux-riscv64-musl': 0.57.0 + '@oxfmt/binding-linux-s390x-gnu': 0.57.0 + '@oxfmt/binding-linux-x64-gnu': 0.57.0 + '@oxfmt/binding-linux-x64-musl': 0.57.0 + '@oxfmt/binding-openharmony-arm64': 0.57.0 + '@oxfmt/binding-win32-arm64-msvc': 0.57.0 + '@oxfmt/binding-win32-ia32-msvc': 0.57.0 + '@oxfmt/binding-win32-x64-msvc': 0.57.0 - oxlint@1.70.0: + oxlint@1.72.0: optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.70.0 - '@oxlint/binding-android-arm64': 1.70.0 - '@oxlint/binding-darwin-arm64': 1.70.0 - '@oxlint/binding-darwin-x64': 1.70.0 - '@oxlint/binding-freebsd-x64': 1.70.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.70.0 - '@oxlint/binding-linux-arm-musleabihf': 1.70.0 - '@oxlint/binding-linux-arm64-gnu': 1.70.0 - '@oxlint/binding-linux-arm64-musl': 1.70.0 - '@oxlint/binding-linux-ppc64-gnu': 1.70.0 - '@oxlint/binding-linux-riscv64-gnu': 1.70.0 - '@oxlint/binding-linux-riscv64-musl': 1.70.0 - '@oxlint/binding-linux-s390x-gnu': 1.70.0 - '@oxlint/binding-linux-x64-gnu': 1.70.0 - '@oxlint/binding-linux-x64-musl': 1.70.0 - '@oxlint/binding-openharmony-arm64': 1.70.0 - '@oxlint/binding-win32-arm64-msvc': 1.70.0 - '@oxlint/binding-win32-ia32-msvc': 1.70.0 - '@oxlint/binding-win32-x64-msvc': 1.70.0 + '@oxlint/binding-android-arm-eabi': 1.72.0 + '@oxlint/binding-android-arm64': 1.72.0 + '@oxlint/binding-darwin-arm64': 1.72.0 + '@oxlint/binding-darwin-x64': 1.72.0 + '@oxlint/binding-freebsd-x64': 1.72.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.72.0 + '@oxlint/binding-linux-arm-musleabihf': 1.72.0 + '@oxlint/binding-linux-arm64-gnu': 1.72.0 + '@oxlint/binding-linux-arm64-musl': 1.72.0 + '@oxlint/binding-linux-ppc64-gnu': 1.72.0 + '@oxlint/binding-linux-riscv64-gnu': 1.72.0 + '@oxlint/binding-linux-riscv64-musl': 1.72.0 + '@oxlint/binding-linux-s390x-gnu': 1.72.0 + '@oxlint/binding-linux-x64-gnu': 1.72.0 + '@oxlint/binding-linux-x64-musl': 1.72.0 + '@oxlint/binding-openharmony-arm64': 1.72.0 + '@oxlint/binding-win32-arm64-msvc': 1.72.0 + '@oxlint/binding-win32-ia32-msvc': 1.72.0 + '@oxlint/binding-win32-x64-msvc': 1.72.0 p-cancelable@3.0.0: {} @@ -26573,8 +26888,6 @@ snapshots: picocolors@1.1.1: {} - picomatch-browser@2.2.6: {} - picomatch@2.3.1: {} picomatch@4.0.4: {} @@ -26625,11 +26938,11 @@ snapshots: pvutils: 1.1.5 tslib: 2.8.1 - playwright-core@1.61.0: {} + playwright-core@1.61.1: {} - playwright@1.61.0: + playwright@1.61.1: dependencies: - playwright-core: 1.61.0 + playwright-core: 1.61.1 optionalDependencies: fsevents: 2.3.2 @@ -26642,431 +26955,431 @@ snapshots: path-data-parser: 0.1.0 points-on-curve: 0.2.0 - postcss-attribute-case-insensitive@7.0.1(postcss@8.5.15): + postcss-attribute-case-insensitive@7.0.1(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-selector-parser: 7.1.0 - postcss-calc@9.0.1(postcss@8.5.15): + postcss-calc@9.0.1(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-selector-parser: 6.1.2 postcss-value-parser: 4.2.0 - postcss-clamp@4.1.0(postcss@8.5.15): + postcss-clamp@4.1.0(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-value-parser: 4.2.0 - postcss-color-functional-notation@7.0.12(postcss@8.5.15): + postcss-color-functional-notation@7.0.12(postcss@8.5.16): dependencies: '@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 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.15) - '@csstools/utilities': 2.0.0(postcss@8.5.15) - postcss: 8.5.15 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.16) + '@csstools/utilities': 2.0.0(postcss@8.5.16) + postcss: 8.5.16 - postcss-color-hex-alpha@10.0.0(postcss@8.5.15): + postcss-color-hex-alpha@10.0.0(postcss@8.5.16): dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.15) - postcss: 8.5.15 + '@csstools/utilities': 2.0.0(postcss@8.5.16) + postcss: 8.5.16 postcss-value-parser: 4.2.0 - postcss-color-rebeccapurple@10.0.0(postcss@8.5.15): + postcss-color-rebeccapurple@10.0.0(postcss@8.5.16): dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.15) - postcss: 8.5.15 + '@csstools/utilities': 2.0.0(postcss@8.5.16) + postcss: 8.5.16 postcss-value-parser: 4.2.0 - postcss-colormin@6.1.0(postcss@8.5.15): + postcss-colormin@6.1.0(postcss@8.5.16): dependencies: browserslist: 4.28.2 caniuse-api: 3.0.0 colord: 2.9.3 - postcss: 8.5.15 + postcss: 8.5.16 postcss-value-parser: 4.2.0 - postcss-convert-values@6.1.0(postcss@8.5.15): + postcss-convert-values@6.1.0(postcss@8.5.16): dependencies: browserslist: 4.28.2 - postcss: 8.5.15 + postcss: 8.5.16 postcss-value-parser: 4.2.0 - postcss-custom-media@11.0.6(postcss@8.5.15): + postcss-custom-media@11.0.6(postcss@8.5.16): dependencies: '@csstools/cascade-layer-name-parser': 2.0.5(@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/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - postcss: 8.5.15 + postcss: 8.5.16 - postcss-custom-properties@14.0.6(postcss@8.5.15): + postcss-custom-properties@14.0.6(postcss@8.5.16): dependencies: '@csstools/cascade-layer-name-parser': 2.0.5(@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/utilities': 2.0.0(postcss@8.5.15) - postcss: 8.5.15 + '@csstools/utilities': 2.0.0(postcss@8.5.16) + postcss: 8.5.16 postcss-value-parser: 4.2.0 - postcss-custom-selectors@8.0.5(postcss@8.5.15): + postcss-custom-selectors@8.0.5(postcss@8.5.16): dependencies: '@csstools/cascade-layer-name-parser': 2.0.5(@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 - postcss: 8.5.15 + postcss: 8.5.16 postcss-selector-parser: 7.1.0 - postcss-dir-pseudo-class@9.0.1(postcss@8.5.15): + postcss-dir-pseudo-class@9.0.1(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-selector-parser: 7.1.0 - postcss-discard-comments@6.0.2(postcss@8.5.15): + postcss-discard-comments@6.0.2(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 - postcss-discard-duplicates@6.0.3(postcss@8.5.15): + postcss-discard-duplicates@6.0.3(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 - postcss-discard-empty@6.0.3(postcss@8.5.15): + postcss-discard-empty@6.0.3(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 - postcss-discard-overridden@6.0.2(postcss@8.5.15): + postcss-discard-overridden@6.0.2(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 - postcss-discard-unused@6.0.5(postcss@8.5.15): + postcss-discard-unused@6.0.5(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-selector-parser: 6.1.2 - postcss-double-position-gradients@6.0.4(postcss@8.5.15): + postcss-double-position-gradients@6.0.4(postcss@8.5.16): dependencies: - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.15) - '@csstools/utilities': 2.0.0(postcss@8.5.15) - postcss: 8.5.15 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.16) + '@csstools/utilities': 2.0.0(postcss@8.5.16) + postcss: 8.5.16 postcss-value-parser: 4.2.0 - postcss-focus-visible@10.0.1(postcss@8.5.15): + postcss-focus-visible@10.0.1(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-selector-parser: 7.1.0 - postcss-focus-within@9.0.1(postcss@8.5.15): + postcss-focus-within@9.0.1(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-selector-parser: 7.1.0 - postcss-font-variant@5.0.0(postcss@8.5.15): + postcss-font-variant@5.0.0(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 - postcss-gap-properties@6.0.0(postcss@8.5.15): + postcss-gap-properties@6.0.0(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 - postcss-image-set-function@7.0.0(postcss@8.5.15): + postcss-image-set-function@7.0.0(postcss@8.5.16): dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.15) - postcss: 8.5.15 + '@csstools/utilities': 2.0.0(postcss@8.5.16) + postcss: 8.5.16 postcss-value-parser: 4.2.0 - postcss-js@4.0.1(postcss@8.5.15): + postcss-js@4.0.1(postcss@8.5.16): dependencies: camelcase-css: 2.0.1 - postcss: 8.5.15 + postcss: 8.5.16 - postcss-lab-function@7.0.12(postcss@8.5.15): + postcss-lab-function@7.0.12(postcss@8.5.16): dependencies: '@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 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.15) - '@csstools/utilities': 2.0.0(postcss@8.5.15) - postcss: 8.5.15 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.16) + '@csstools/utilities': 2.0.0(postcss@8.5.16) + postcss: 8.5.16 - postcss-loader@7.3.4(postcss@8.5.15)(typescript@6.0.3)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)): + postcss-loader@7.3.4(postcss@8.5.16)(typescript@6.0.3)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)): dependencies: cosmiconfig: 8.3.6(typescript@6.0.3) jiti: 1.21.7 - postcss: 8.5.15 + postcss: 8.5.16 semver: 7.7.4 - webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16) transitivePeerDependencies: - typescript - postcss-logical@8.1.0(postcss@8.5.15): + postcss-logical@8.1.0(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-value-parser: 4.2.0 - postcss-merge-idents@6.0.3(postcss@8.5.15): + postcss-merge-idents@6.0.3(postcss@8.5.16): dependencies: - cssnano-utils: 4.0.2(postcss@8.5.15) - postcss: 8.5.15 + cssnano-utils: 4.0.2(postcss@8.5.16) + postcss: 8.5.16 postcss-value-parser: 4.2.0 - postcss-merge-longhand@6.0.5(postcss@8.5.15): + postcss-merge-longhand@6.0.5(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-value-parser: 4.2.0 - stylehacks: 6.1.1(postcss@8.5.15) + stylehacks: 6.1.1(postcss@8.5.16) - postcss-merge-rules@6.1.1(postcss@8.5.15): + postcss-merge-rules@6.1.1(postcss@8.5.16): dependencies: browserslist: 4.28.2 caniuse-api: 3.0.0 - cssnano-utils: 4.0.2(postcss@8.5.15) - postcss: 8.5.15 + cssnano-utils: 4.0.2(postcss@8.5.16) + postcss: 8.5.16 postcss-selector-parser: 6.1.2 - postcss-minify-font-values@6.1.0(postcss@8.5.15): + postcss-minify-font-values@6.1.0(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-value-parser: 4.2.0 - postcss-minify-gradients@6.0.3(postcss@8.5.15): + postcss-minify-gradients@6.0.3(postcss@8.5.16): dependencies: colord: 2.9.3 - cssnano-utils: 4.0.2(postcss@8.5.15) - postcss: 8.5.15 + cssnano-utils: 4.0.2(postcss@8.5.16) + postcss: 8.5.16 postcss-value-parser: 4.2.0 - postcss-minify-params@6.1.0(postcss@8.5.15): + postcss-minify-params@6.1.0(postcss@8.5.16): dependencies: browserslist: 4.28.2 - cssnano-utils: 4.0.2(postcss@8.5.15) - postcss: 8.5.15 + cssnano-utils: 4.0.2(postcss@8.5.16) + postcss: 8.5.16 postcss-value-parser: 4.2.0 - postcss-minify-selectors@6.0.4(postcss@8.5.15): + postcss-minify-selectors@6.0.4(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-selector-parser: 6.1.2 - postcss-mixins@12.0.0(postcss@8.5.15): + postcss-mixins@12.0.0(postcss@8.5.16): dependencies: - postcss: 8.5.15 - postcss-js: 4.0.1(postcss@8.5.15) - postcss-simple-vars: 7.0.1(postcss@8.5.15) - sugarss: 5.0.0(postcss@8.5.15) + postcss: 8.5.16 + postcss-js: 4.0.1(postcss@8.5.16) + postcss-simple-vars: 7.0.1(postcss@8.5.16) + sugarss: 5.0.0(postcss@8.5.16) tinyglobby: 0.2.17 - postcss-modules-extract-imports@3.1.0(postcss@8.5.15): + postcss-modules-extract-imports@3.1.0(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 - postcss-modules-local-by-default@4.2.0(postcss@8.5.15): + postcss-modules-local-by-default@4.2.0(postcss@8.5.16): dependencies: - icss-utils: 5.1.0(postcss@8.5.15) - postcss: 8.5.15 + icss-utils: 5.1.0(postcss@8.5.16) + postcss: 8.5.16 postcss-selector-parser: 7.1.0 postcss-value-parser: 4.2.0 - postcss-modules-scope@3.2.1(postcss@8.5.15): + postcss-modules-scope@3.2.1(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-selector-parser: 7.1.0 - postcss-modules-values@4.0.0(postcss@8.5.15): + postcss-modules-values@4.0.0(postcss@8.5.16): dependencies: - icss-utils: 5.1.0(postcss@8.5.15) - postcss: 8.5.15 + icss-utils: 5.1.0(postcss@8.5.16) + postcss: 8.5.16 - postcss-nested@7.0.2(postcss@8.5.15): + postcss-nested@7.0.2(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-selector-parser: 7.1.0 - postcss-nesting@13.0.2(postcss@8.5.15): + postcss-nesting@13.0.2(postcss@8.5.16): dependencies: '@csstools/selector-resolve-nested': 3.1.0(postcss-selector-parser@7.1.0) '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0) - postcss: 8.5.15 + postcss: 8.5.16 postcss-selector-parser: 7.1.0 - postcss-normalize-charset@6.0.2(postcss@8.5.15): + postcss-normalize-charset@6.0.2(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 - postcss-normalize-display-values@6.0.2(postcss@8.5.15): + postcss-normalize-display-values@6.0.2(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-value-parser: 4.2.0 - postcss-normalize-positions@6.0.2(postcss@8.5.15): + postcss-normalize-positions@6.0.2(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-value-parser: 4.2.0 - postcss-normalize-repeat-style@6.0.2(postcss@8.5.15): + postcss-normalize-repeat-style@6.0.2(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-value-parser: 4.2.0 - postcss-normalize-string@6.0.2(postcss@8.5.15): + postcss-normalize-string@6.0.2(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-value-parser: 4.2.0 - postcss-normalize-timing-functions@6.0.2(postcss@8.5.15): + postcss-normalize-timing-functions@6.0.2(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-value-parser: 4.2.0 - postcss-normalize-unicode@6.1.0(postcss@8.5.15): + postcss-normalize-unicode@6.1.0(postcss@8.5.16): dependencies: browserslist: 4.28.2 - postcss: 8.5.15 + postcss: 8.5.16 postcss-value-parser: 4.2.0 - postcss-normalize-url@6.0.2(postcss@8.5.15): + postcss-normalize-url@6.0.2(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-value-parser: 4.2.0 - postcss-normalize-whitespace@6.0.2(postcss@8.5.15): + postcss-normalize-whitespace@6.0.2(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-value-parser: 4.2.0 - postcss-opacity-percentage@3.0.0(postcss@8.5.15): + postcss-opacity-percentage@3.0.0(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 - postcss-ordered-values@6.0.2(postcss@8.5.15): + postcss-ordered-values@6.0.2(postcss@8.5.16): dependencies: - cssnano-utils: 4.0.2(postcss@8.5.15) - postcss: 8.5.15 + cssnano-utils: 4.0.2(postcss@8.5.16) + postcss: 8.5.16 postcss-value-parser: 4.2.0 - postcss-overflow-shorthand@6.0.0(postcss@8.5.15): + postcss-overflow-shorthand@6.0.0(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-value-parser: 4.2.0 - postcss-page-break@3.0.4(postcss@8.5.15): + postcss-page-break@3.0.4(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 - postcss-place@10.0.0(postcss@8.5.15): + postcss-place@10.0.0(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-value-parser: 4.2.0 - postcss-preset-env@10.6.1(postcss@8.5.15): + postcss-preset-env@10.6.1(postcss@8.5.16): dependencies: - '@csstools/postcss-alpha-function': 1.0.1(postcss@8.5.15) - '@csstools/postcss-cascade-layers': 5.0.2(postcss@8.5.15) - '@csstools/postcss-color-function': 4.0.12(postcss@8.5.15) - '@csstools/postcss-color-function-display-p3-linear': 1.0.1(postcss@8.5.15) - '@csstools/postcss-color-mix-function': 3.0.12(postcss@8.5.15) - '@csstools/postcss-color-mix-variadic-function-arguments': 1.0.2(postcss@8.5.15) - '@csstools/postcss-content-alt-text': 2.0.8(postcss@8.5.15) - '@csstools/postcss-contrast-color-function': 2.0.12(postcss@8.5.15) - '@csstools/postcss-exponential-functions': 2.0.9(postcss@8.5.15) - '@csstools/postcss-font-format-keywords': 4.0.0(postcss@8.5.15) - '@csstools/postcss-gamut-mapping': 2.0.11(postcss@8.5.15) - '@csstools/postcss-gradients-interpolation-method': 5.0.12(postcss@8.5.15) - '@csstools/postcss-hwb-function': 4.0.12(postcss@8.5.15) - '@csstools/postcss-ic-unit': 4.0.4(postcss@8.5.15) - '@csstools/postcss-initial': 2.0.1(postcss@8.5.15) - '@csstools/postcss-is-pseudo-class': 5.0.3(postcss@8.5.15) - '@csstools/postcss-light-dark-function': 2.0.11(postcss@8.5.15) - '@csstools/postcss-logical-float-and-clear': 3.0.0(postcss@8.5.15) - '@csstools/postcss-logical-overflow': 2.0.0(postcss@8.5.15) - '@csstools/postcss-logical-overscroll-behavior': 2.0.0(postcss@8.5.15) - '@csstools/postcss-logical-resize': 3.0.0(postcss@8.5.15) - '@csstools/postcss-logical-viewport-units': 3.0.4(postcss@8.5.15) - '@csstools/postcss-media-minmax': 2.0.9(postcss@8.5.15) - '@csstools/postcss-media-queries-aspect-ratio-number-values': 3.0.5(postcss@8.5.15) - '@csstools/postcss-nested-calc': 4.0.0(postcss@8.5.15) - '@csstools/postcss-normalize-display-values': 4.0.1(postcss@8.5.15) - '@csstools/postcss-oklab-function': 4.0.12(postcss@8.5.15) - '@csstools/postcss-position-area-property': 1.0.0(postcss@8.5.15) - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.15) - '@csstools/postcss-property-rule-prelude-list': 1.0.0(postcss@8.5.15) - '@csstools/postcss-random-function': 2.0.1(postcss@8.5.15) - '@csstools/postcss-relative-color-syntax': 3.0.12(postcss@8.5.15) - '@csstools/postcss-scope-pseudo-class': 4.0.1(postcss@8.5.15) - '@csstools/postcss-sign-functions': 1.1.4(postcss@8.5.15) - '@csstools/postcss-stepped-value-functions': 4.0.9(postcss@8.5.15) - '@csstools/postcss-syntax-descriptor-syntax-production': 1.0.1(postcss@8.5.15) - '@csstools/postcss-system-ui-font-family': 1.0.0(postcss@8.5.15) - '@csstools/postcss-text-decoration-shorthand': 4.0.3(postcss@8.5.15) - '@csstools/postcss-trigonometric-functions': 4.0.9(postcss@8.5.15) - '@csstools/postcss-unset-value': 4.0.0(postcss@8.5.15) - autoprefixer: 10.5.0(postcss@8.5.15) + '@csstools/postcss-alpha-function': 1.0.1(postcss@8.5.16) + '@csstools/postcss-cascade-layers': 5.0.2(postcss@8.5.16) + '@csstools/postcss-color-function': 4.0.12(postcss@8.5.16) + '@csstools/postcss-color-function-display-p3-linear': 1.0.1(postcss@8.5.16) + '@csstools/postcss-color-mix-function': 3.0.12(postcss@8.5.16) + '@csstools/postcss-color-mix-variadic-function-arguments': 1.0.2(postcss@8.5.16) + '@csstools/postcss-content-alt-text': 2.0.8(postcss@8.5.16) + '@csstools/postcss-contrast-color-function': 2.0.12(postcss@8.5.16) + '@csstools/postcss-exponential-functions': 2.0.9(postcss@8.5.16) + '@csstools/postcss-font-format-keywords': 4.0.0(postcss@8.5.16) + '@csstools/postcss-gamut-mapping': 2.0.11(postcss@8.5.16) + '@csstools/postcss-gradients-interpolation-method': 5.0.12(postcss@8.5.16) + '@csstools/postcss-hwb-function': 4.0.12(postcss@8.5.16) + '@csstools/postcss-ic-unit': 4.0.4(postcss@8.5.16) + '@csstools/postcss-initial': 2.0.1(postcss@8.5.16) + '@csstools/postcss-is-pseudo-class': 5.0.3(postcss@8.5.16) + '@csstools/postcss-light-dark-function': 2.0.11(postcss@8.5.16) + '@csstools/postcss-logical-float-and-clear': 3.0.0(postcss@8.5.16) + '@csstools/postcss-logical-overflow': 2.0.0(postcss@8.5.16) + '@csstools/postcss-logical-overscroll-behavior': 2.0.0(postcss@8.5.16) + '@csstools/postcss-logical-resize': 3.0.0(postcss@8.5.16) + '@csstools/postcss-logical-viewport-units': 3.0.4(postcss@8.5.16) + '@csstools/postcss-media-minmax': 2.0.9(postcss@8.5.16) + '@csstools/postcss-media-queries-aspect-ratio-number-values': 3.0.5(postcss@8.5.16) + '@csstools/postcss-nested-calc': 4.0.0(postcss@8.5.16) + '@csstools/postcss-normalize-display-values': 4.0.1(postcss@8.5.16) + '@csstools/postcss-oklab-function': 4.0.12(postcss@8.5.16) + '@csstools/postcss-position-area-property': 1.0.0(postcss@8.5.16) + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.16) + '@csstools/postcss-property-rule-prelude-list': 1.0.0(postcss@8.5.16) + '@csstools/postcss-random-function': 2.0.1(postcss@8.5.16) + '@csstools/postcss-relative-color-syntax': 3.0.12(postcss@8.5.16) + '@csstools/postcss-scope-pseudo-class': 4.0.1(postcss@8.5.16) + '@csstools/postcss-sign-functions': 1.1.4(postcss@8.5.16) + '@csstools/postcss-stepped-value-functions': 4.0.9(postcss@8.5.16) + '@csstools/postcss-syntax-descriptor-syntax-production': 1.0.1(postcss@8.5.16) + '@csstools/postcss-system-ui-font-family': 1.0.0(postcss@8.5.16) + '@csstools/postcss-text-decoration-shorthand': 4.0.3(postcss@8.5.16) + '@csstools/postcss-trigonometric-functions': 4.0.9(postcss@8.5.16) + '@csstools/postcss-unset-value': 4.0.0(postcss@8.5.16) + autoprefixer: 10.5.0(postcss@8.5.16) browserslist: 4.28.2 - css-blank-pseudo: 7.0.1(postcss@8.5.15) - css-has-pseudo: 7.0.3(postcss@8.5.15) - css-prefers-color-scheme: 10.0.0(postcss@8.5.15) + css-blank-pseudo: 7.0.1(postcss@8.5.16) + css-has-pseudo: 7.0.3(postcss@8.5.16) + css-prefers-color-scheme: 10.0.0(postcss@8.5.16) cssdb: 8.9.0 - postcss: 8.5.15 - postcss-attribute-case-insensitive: 7.0.1(postcss@8.5.15) - postcss-clamp: 4.1.0(postcss@8.5.15) - postcss-color-functional-notation: 7.0.12(postcss@8.5.15) - postcss-color-hex-alpha: 10.0.0(postcss@8.5.15) - postcss-color-rebeccapurple: 10.0.0(postcss@8.5.15) - postcss-custom-media: 11.0.6(postcss@8.5.15) - postcss-custom-properties: 14.0.6(postcss@8.5.15) - postcss-custom-selectors: 8.0.5(postcss@8.5.15) - postcss-dir-pseudo-class: 9.0.1(postcss@8.5.15) - postcss-double-position-gradients: 6.0.4(postcss@8.5.15) - postcss-focus-visible: 10.0.1(postcss@8.5.15) - postcss-focus-within: 9.0.1(postcss@8.5.15) - postcss-font-variant: 5.0.0(postcss@8.5.15) - postcss-gap-properties: 6.0.0(postcss@8.5.15) - postcss-image-set-function: 7.0.0(postcss@8.5.15) - postcss-lab-function: 7.0.12(postcss@8.5.15) - postcss-logical: 8.1.0(postcss@8.5.15) - postcss-nesting: 13.0.2(postcss@8.5.15) - postcss-opacity-percentage: 3.0.0(postcss@8.5.15) - postcss-overflow-shorthand: 6.0.0(postcss@8.5.15) - postcss-page-break: 3.0.4(postcss@8.5.15) - postcss-place: 10.0.0(postcss@8.5.15) - postcss-pseudo-class-any-link: 10.0.1(postcss@8.5.15) - postcss-replace-overflow-wrap: 4.0.0(postcss@8.5.15) - postcss-selector-not: 8.0.1(postcss@8.5.15) + postcss: 8.5.16 + postcss-attribute-case-insensitive: 7.0.1(postcss@8.5.16) + postcss-clamp: 4.1.0(postcss@8.5.16) + postcss-color-functional-notation: 7.0.12(postcss@8.5.16) + postcss-color-hex-alpha: 10.0.0(postcss@8.5.16) + postcss-color-rebeccapurple: 10.0.0(postcss@8.5.16) + postcss-custom-media: 11.0.6(postcss@8.5.16) + postcss-custom-properties: 14.0.6(postcss@8.5.16) + postcss-custom-selectors: 8.0.5(postcss@8.5.16) + postcss-dir-pseudo-class: 9.0.1(postcss@8.5.16) + postcss-double-position-gradients: 6.0.4(postcss@8.5.16) + postcss-focus-visible: 10.0.1(postcss@8.5.16) + postcss-focus-within: 9.0.1(postcss@8.5.16) + postcss-font-variant: 5.0.0(postcss@8.5.16) + postcss-gap-properties: 6.0.0(postcss@8.5.16) + postcss-image-set-function: 7.0.0(postcss@8.5.16) + postcss-lab-function: 7.0.12(postcss@8.5.16) + postcss-logical: 8.1.0(postcss@8.5.16) + postcss-nesting: 13.0.2(postcss@8.5.16) + postcss-opacity-percentage: 3.0.0(postcss@8.5.16) + postcss-overflow-shorthand: 6.0.0(postcss@8.5.16) + postcss-page-break: 3.0.4(postcss@8.5.16) + postcss-place: 10.0.0(postcss@8.5.16) + postcss-pseudo-class-any-link: 10.0.1(postcss@8.5.16) + postcss-replace-overflow-wrap: 4.0.0(postcss@8.5.16) + postcss-selector-not: 8.0.1(postcss@8.5.16) - postcss-preset-mantine@1.18.0(postcss@8.5.15): + postcss-preset-mantine@1.18.0(postcss@8.5.16): dependencies: - postcss: 8.5.15 - postcss-mixins: 12.0.0(postcss@8.5.15) - postcss-nested: 7.0.2(postcss@8.5.15) + postcss: 8.5.16 + postcss-mixins: 12.0.0(postcss@8.5.16) + postcss-nested: 7.0.2(postcss@8.5.16) - postcss-pseudo-class-any-link@10.0.1(postcss@8.5.15): + postcss-pseudo-class-any-link@10.0.1(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-selector-parser: 7.1.0 - postcss-reduce-idents@6.0.3(postcss@8.5.15): + postcss-reduce-idents@6.0.3(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-value-parser: 4.2.0 - postcss-reduce-initial@6.1.0(postcss@8.5.15): + postcss-reduce-initial@6.1.0(postcss@8.5.16): dependencies: browserslist: 4.28.2 caniuse-api: 3.0.0 - postcss: 8.5.15 + postcss: 8.5.16 - postcss-reduce-transforms@6.0.2(postcss@8.5.15): + postcss-reduce-transforms@6.0.2(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-value-parser: 4.2.0 - postcss-replace-overflow-wrap@4.0.0(postcss@8.5.15): + postcss-replace-overflow-wrap@4.0.0(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 - postcss-selector-not@8.0.1(postcss@8.5.15): + postcss-selector-not@8.0.1(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-selector-parser: 7.1.0 postcss-selector-parser@6.1.2: @@ -27079,31 +27392,31 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-simple-vars@7.0.1(postcss@8.5.15): + postcss-simple-vars@7.0.1(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 - postcss-sort-media-queries@5.2.0(postcss@8.5.15): + postcss-sort-media-queries@5.2.0(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 sort-css-media-queries: 2.2.0 - postcss-svgo@6.0.3(postcss@8.5.15): + postcss-svgo@6.0.3(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-value-parser: 4.2.0 svgo: 3.3.3 - postcss-unique-selectors@6.0.4(postcss@8.5.15): + postcss-unique-selectors@6.0.4(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-selector-parser: 6.1.2 postcss-value-parser@4.2.0: {} - postcss-zindex@6.0.2(postcss@8.5.15): + postcss-zindex@6.0.2(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss@8.4.31: dependencies: @@ -27117,6 +27430,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.16: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postgres-array@2.0.0: {} postgres-bytea@1.0.0: {} @@ -27129,22 +27448,22 @@ snapshots: posthog-docusaurus@2.0.5: {} - posthog-js@1.391.2: + posthog-js@1.396.5: dependencies: - '@posthog/core': 1.35.3 - '@posthog/types': 1.390.2 + '@posthog/core': 1.39.6 + '@posthog/types': 1.392.0 core-js: 3.49.0 - dompurify: 3.4.10 + dompurify: 3.4.11 fflate: 0.4.8 preact: 10.29.2 query-selector-shadow-dom: 1.0.1 web-vitals: 5.3.0 - posthog-node@4.18.0: + posthog-node@5.39.4(rxjs@7.8.2): dependencies: - axios: 1.15.0 - transitivePeerDependencies: - - debug + '@posthog/core': 1.39.6 + optionalDependencies: + rxjs: 7.8.2 preact-render-to-string@6.5.11(preact@10.24.3): dependencies: @@ -27166,7 +27485,7 @@ snapshots: pump: 3.0.2 rc: 1.2.8 simple-get: 4.0.1 - tar-fs: 3.1.2 + tar-fs: 3.1.3 tunnel-agent: 0.6.0 transitivePeerDependencies: - bare-buffer @@ -27317,7 +27636,7 @@ snapshots: proxmox-api@1.1.1: dependencies: - undici: 7.27.1 + undici: 7.28.0 proxy-addr@2.0.7: dependencies: @@ -27382,7 +27701,7 @@ snapshots: aria-hidden: 1.2.6 defu: 6.1.7 fast-deep-equal: 3.1.3 - nanoid: 5.1.15 + nanoid: 5.1.16 vue: 3.5.35(typescript@6.0.3) transitivePeerDependencies: - '@vue/composition-api' @@ -27399,8 +27718,6 @@ snapshots: range-parser@1.2.1: {} - rate-limiter-flexible@8.1.0: {} - raw-body@2.5.3: dependencies: bytes: 3.1.2 @@ -27456,11 +27773,11 @@ snapshots: '@types/react': 18.3.31 '@types/react-dom': 18.3.7(@types/react@18.3.31) - react-loadable-ssr-addon-v5-slorber@1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.7))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)): + react-loadable-ssr-addon-v5-slorber@1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.7))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)): dependencies: '@babel/runtime': 7.29.7 react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.7)' - webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16) react-markdown@10.1.0(@types/react@19.2.17)(react@19.2.7): dependencies: @@ -27644,19 +27961,19 @@ snapshots: real-require@0.2.0: {} - recharts@3.8.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1): + recharts@3.9.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1): dependencies: '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1))(react@19.2.7) clsx: 2.1.1 decimal.js-light: 2.5.1 es-toolkit: 1.46.1 eventemitter3: 5.0.4 - immer: 10.2.0 + immer: 11.1.9 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) react-is: 19.2.7 react-redux: 9.2.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1) - reselect: 5.1.1 + reselect: 5.2.0 tiny-invariant: 1.3.3 use-sync-external-store: 1.6.0(react@19.2.7) victory-vendor: 37.3.6 @@ -27894,6 +28211,8 @@ snapshots: reselect@5.1.1: {} + reselect@5.2.0: {} + reserved-identifiers@1.2.0: {} resolve-alpn@1.2.1: {} @@ -27938,26 +28257,26 @@ snapshots: robust-predicates@3.0.3: {} - rolldown@1.0.3: + rolldown@1.1.4: dependencies: - '@oxc-project/types': 0.133.0 + '@oxc-project/types': 0.138.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.3 - '@rolldown/binding-darwin-arm64': 1.0.3 - '@rolldown/binding-darwin-x64': 1.0.3 - '@rolldown/binding-freebsd-x64': 1.0.3 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 - '@rolldown/binding-linux-arm64-gnu': 1.0.3 - '@rolldown/binding-linux-arm64-musl': 1.0.3 - '@rolldown/binding-linux-ppc64-gnu': 1.0.3 - '@rolldown/binding-linux-s390x-gnu': 1.0.3 - '@rolldown/binding-linux-x64-gnu': 1.0.3 - '@rolldown/binding-linux-x64-musl': 1.0.3 - '@rolldown/binding-openharmony-arm64': 1.0.3 - '@rolldown/binding-wasm32-wasi': 1.0.3 - '@rolldown/binding-win32-arm64-msvc': 1.0.3 - '@rolldown/binding-win32-x64-msvc': 1.0.3 + '@rolldown/binding-android-arm64': 1.1.4 + '@rolldown/binding-darwin-arm64': 1.1.4 + '@rolldown/binding-darwin-x64': 1.1.4 + '@rolldown/binding-freebsd-x64': 1.1.4 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.4 + '@rolldown/binding-linux-arm64-gnu': 1.1.4 + '@rolldown/binding-linux-arm64-musl': 1.1.4 + '@rolldown/binding-linux-ppc64-gnu': 1.1.4 + '@rolldown/binding-linux-s390x-gnu': 1.1.4 + '@rolldown/binding-linux-x64-gnu': 1.1.4 + '@rolldown/binding-linux-x64-musl': 1.1.4 + '@rolldown/binding-openharmony-arm64': 1.1.4 + '@rolldown/binding-wasm32-wasi': 1.1.4 + '@rolldown/binding-win32-arm64-msvc': 1.1.4 + '@rolldown/binding-win32-x64-msvc': 1.1.4 rope-sequence@1.3.4: {} @@ -27984,7 +28303,7 @@ snapshots: dependencies: escalade: 3.2.0 picocolors: 1.1.1 - postcss: 8.5.15 + postcss: 8.5.16 strip-json-comments: 3.1.1 run-applescript@7.1.0: {} @@ -28062,7 +28381,7 @@ snapshots: dependencies: '@semantic-release/commit-analyzer': 13.0.1(semantic-release@25.0.5(typescript@6.0.3)) '@semantic-release/error': 4.0.0 - '@semantic-release/github': 12.0.8(semantic-release@25.0.5(typescript@6.0.3)) + '@semantic-release/github': 12.0.9(semantic-release@25.0.5(typescript@6.0.3)) '@semantic-release/npm': 13.1.5(semantic-release@25.0.5(typescript@6.0.3)) '@semantic-release/release-notes-generator': 14.1.1(semantic-release@25.0.5(typescript@6.0.3)) aggregate-error: 5.0.0 @@ -28538,6 +28857,8 @@ snapshots: strip-json-comments@3.1.1: {} + strnum@2.3.0: {} + strnum@2.4.1: dependencies: anynum: 1.0.1 @@ -28559,17 +28880,17 @@ snapshots: optionalDependencies: '@babel/core': 7.29.0 - stylehacks@6.1.1(postcss@8.5.15): + stylehacks@6.1.1(postcss@8.5.16): dependencies: browserslist: 4.28.2 - postcss: 8.5.15 + postcss: 8.5.16 postcss-selector-parser: 6.1.2 stylis@4.4.0: {} - sugarss@5.0.0(postcss@8.5.15): + sugarss@5.0.0(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 super-regex@1.0.0: dependencies: @@ -28618,13 +28939,13 @@ snapshots: deep-rename-keys: 0.2.1 xml-reader: 2.4.3 - swc-loader@0.2.7(@swc/core@1.15.3(@swc/helpers@0.5.15))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)): + swc-loader@0.2.7(@swc/core@1.15.3(@swc/helpers@0.5.15))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)): dependencies: '@swc/core': 1.15.3(@swc/helpers@0.5.15) '@swc/counter': 0.1.3 - webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16) - swiper@12.2.0: {} + swiper@14.0.1: {} swrv@1.2.0(vue@3.5.35(typescript@6.0.3)): dependencies: @@ -28638,7 +28959,7 @@ snapshots: tailwind-merge@3.5.0: {} - tailwindcss@4.3.1: {} + tailwindcss@4.3.2: {} tapable@2.3.3: {} @@ -28659,6 +28980,16 @@ snapshots: transitivePeerDependencies: - bare-buffer + tar-fs@3.1.3: + dependencies: + pump: 3.0.2 + tar-stream: 3.1.7 + optionalDependencies: + bare-fs: 4.1.2 + bare-path: 3.0.0 + transitivePeerDependencies: + - bare-buffer + tar-stream@2.2.0: dependencies: bl: 4.1.0 @@ -28682,45 +29013,60 @@ snapshots: type-fest: 2.19.0 unique-string: 3.0.0 - terser-webpack-plugin@5.6.0(@swc/core@1.15.3(@swc/helpers@0.5.15))(@swc/html@1.15.40)(esbuild@0.28.0)(lightningcss@1.32.0)(postcss@8.5.15)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)): + terser-webpack-plugin@5.6.0(@swc/core@1.15.3(@swc/helpers@0.5.15))(@swc/html@1.15.40)(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.16)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.46.1 - webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16) optionalDependencies: '@swc/core': 1.15.3(@swc/helpers@0.5.15) '@swc/html': 1.15.40 - esbuild: 0.28.0 + esbuild: 0.28.1 lightningcss: 1.32.0 - postcss: 8.5.15 + postcss: 8.5.16 - terser-webpack-plugin@5.6.0(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)): + terser-webpack-plugin@5.6.0(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.16)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.46.1 - webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16) optionalDependencies: '@swc/core': 1.15.3(@swc/helpers@0.5.15) clean-css: 5.3.3 - cssnano: 6.1.2(postcss@8.5.15) + cssnano: 6.1.2(postcss@8.5.16) esbuild: 0.28.0 html-minifier-terser: 7.2.0 - postcss: 8.5.15 + postcss: 8.5.16 - terser-webpack-plugin@5.6.0(esbuild@0.28.0)(postcss@8.5.15)(webpack@5.107.1(esbuild@0.28.0)(postcss@8.5.15)): + terser-webpack-plugin@5.6.0(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.46.1 - webpack: 5.107.1(esbuild@0.28.0)(postcss@8.5.15) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16) optionalDependencies: - esbuild: 0.28.0 - postcss: 8.5.15 + '@swc/core': 1.15.3(@swc/helpers@0.5.15) + clean-css: 5.3.3 + cssnano: 6.1.2(postcss@8.5.16) + esbuild: 0.28.1 + html-minifier-terser: 7.2.0 + postcss: 8.5.16 + + terser-webpack-plugin@5.6.0(esbuild@0.28.1)(postcss@8.5.16)(webpack@5.107.1(esbuild@0.28.1)(postcss@8.5.16)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.46.1 + webpack: 5.107.1(esbuild@0.28.1)(postcss@8.5.16) + optionalDependencies: + esbuild: 0.28.1 + postcss: 8.5.16 terser@5.46.1: dependencies: @@ -28729,7 +29075,7 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 - testcontainers@12.0.3: + testcontainers@12.0.4: dependencies: '@balena/dockerignore': 1.0.2 '@types/dockerode': 4.0.1 @@ -28739,13 +29085,13 @@ snapshots: debug: 4.4.3 docker-compose: 1.4.2 dockerode: 5.0.0 - get-port: 7.2.0 + get-port: 5.1.1 proper-lockfile: 4.1.2 properties-reader: 3.0.1 ssh-remote-port-forward: 1.0.4 tar-fs: 3.1.2 tmp: 0.2.7 - undici: 8.4.1 + undici: 8.6.0 transitivePeerDependencies: - bare-buffer - supports-color @@ -28858,31 +29204,31 @@ snapshots: trough@2.2.0: {} - trpc-to-mcp@1.3.2(@trpc/server@11.17.0(typescript@6.0.3))(better-auth@1.6.15(@opentelemetry/api@1.9.0)(better-sqlite3@12.10.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1))(mysql2@3.22.5(@types/node@24.13.2))(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(pg@8.22.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)(vue@3.5.35(typescript@6.0.3)))(mcp-handler@1.1.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)))(zod@4.4.3): + trpc-to-mcp@1.3.2(@trpc/server@11.18.0(typescript@6.0.3))(better-auth@1.6.15(@opentelemetry/api@1.9.0)(better-sqlite3@12.11.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1))(mysql2@3.22.5(@types/node@24.13.2))(next@16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(pg@8.22.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)(vue@3.5.35(typescript@6.0.3)))(mcp-handler@1.1.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(next@16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)))(zod@4.4.3): dependencies: '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) - '@trpc/server': 11.17.0(typescript@6.0.3) - better-auth: 1.6.15(@opentelemetry/api@1.9.0)(better-sqlite3@12.10.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1))(mysql2@3.22.5(@types/node@24.13.2))(next@16.2.9(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(pg@8.22.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)(vue@3.5.35(typescript@6.0.3)) - mcp-handler: 1.1.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)) + '@trpc/server': 11.18.0(typescript@6.0.3) + better-auth: 1.6.15(@opentelemetry/api@1.9.0)(better-sqlite3@12.11.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1))(mysql2@3.22.5(@types/node@24.13.2))(next@16.2.10(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(pg@8.22.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)(vue@3.5.35(typescript@6.0.3)) + mcp-handler: 1.1.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(next@16.2.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)) zod: 4.4.3 transitivePeerDependencies: - '@cfworker/json-schema' - supports-color - trpc-to-mcp@1.3.2(@trpc/server@11.17.0(typescript@6.0.3))(better-auth@1.6.15(@opentelemetry/api@1.9.0)(better-sqlite3@12.10.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1))(mysql2@3.22.5(@types/node@24.13.2))(next@16.2.9(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(pg@8.22.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)(vue@3.5.35(typescript@6.0.3)))(mcp-handler@1.1.0(@modelcontextprotocol/sdk@1.26.0(zod@4.4.3))(next@16.2.9(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)))(zod@4.4.3): + trpc-to-mcp@1.3.2(@trpc/server@11.18.0(typescript@6.0.3))(better-auth@1.6.15(@opentelemetry/api@1.9.0)(better-sqlite3@12.11.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1))(mysql2@3.22.5(@types/node@24.13.2))(next@16.2.10(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(pg@8.22.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)(vue@3.5.35(typescript@6.0.3)))(mcp-handler@1.1.0(@modelcontextprotocol/sdk@1.26.0(zod@4.4.3))(next@16.2.10(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)))(zod@4.4.3): dependencies: '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) - '@trpc/server': 11.17.0(typescript@6.0.3) - better-auth: 1.6.15(@opentelemetry/api@1.9.0)(better-sqlite3@12.10.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1))(mysql2@3.22.5(@types/node@24.13.2))(next@16.2.9(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(pg@8.22.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)(vue@3.5.35(typescript@6.0.3)) - mcp-handler: 1.1.0(@modelcontextprotocol/sdk@1.26.0(zod@4.4.3))(next@16.2.9(@opentelemetry/api@1.9.0)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)) + '@trpc/server': 11.18.0(typescript@6.0.3) + better-auth: 1.6.15(@opentelemetry/api@1.9.0)(better-sqlite3@12.11.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(gel@2.0.0)(kysely@0.29.2)(mysql2@3.22.5(@types/node@24.13.2))(pg@8.22.0)(sql.js@1.14.1))(mysql2@3.22.5(@types/node@24.13.2))(next@16.2.10(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(pg@8.22.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)(vue@3.5.35(typescript@6.0.3)) + mcp-handler: 1.1.0(@modelcontextprotocol/sdk@1.26.0(zod@4.4.3))(next@16.2.10(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)) zod: 4.4.3 transitivePeerDependencies: - '@cfworker/json-schema' - supports-color - trpc-to-openapi@3.3.0(patch_hash=2ca3c16af0fcca0c736697ad4fe553a14f794524fa9ce0d5c3e8ee4aea76090c)(@trpc/server@11.17.0(typescript@6.0.3))(zod-openapi@5.3.0(zod@4.4.3))(zod@4.4.3): + trpc-to-openapi@3.3.0(patch_hash=2ca3c16af0fcca0c736697ad4fe553a14f794524fa9ce0d5c3e8ee4aea76090c)(@trpc/server@11.18.0(typescript@6.0.3))(zod-openapi@5.3.0(zod@4.4.3))(zod@4.4.3): dependencies: - '@trpc/server': 11.17.0(typescript@6.0.3) + '@trpc/server': 11.18.0(typescript@6.0.3) co-body: 6.2.0 h3: 1.15.11 openapi3-ts: 4.4.0 @@ -28903,9 +29249,8 @@ snapshots: optionalDependencies: typescript: 6.0.3 - tsdav@2.2.2: + tsdav@2.3.0: dependencies: - base-64: 1.0.0 debug: 4.4.3 xml-js: 1.6.11 transitivePeerDependencies: @@ -28941,14 +29286,14 @@ snapshots: tunnel@0.0.6: {} - turbo@2.9.18: + turbo@2.10.2: optionalDependencies: - '@turbo/darwin-64': 2.9.18 - '@turbo/darwin-arm64': 2.9.18 - '@turbo/linux-64': 2.9.18 - '@turbo/linux-arm64': 2.9.18 - '@turbo/windows-64': 2.9.18 - '@turbo/windows-arm64': 2.9.18 + '@turbo/darwin-64': 2.10.2 + '@turbo/darwin-arm64': 2.10.2 + '@turbo/linux-64': 2.10.2 + '@turbo/linux-arm64': 2.10.2 + '@turbo/windows-64': 2.10.2 + '@turbo/windows-arm64': 2.10.2 tweetnacl@0.14.5: {} @@ -28962,6 +29307,10 @@ snapshots: dependencies: tagged-tag: 1.0.0 + type-fest@5.7.0: + dependencies: + tagged-tag: 1.0.0 + type-is@1.6.18: dependencies: media-typer: 0.3.0 @@ -28996,11 +29345,9 @@ snapshots: undici@7.25.0: {} - undici@7.27.1: {} + undici@7.28.0: {} - undici@7.27.2: {} - - undici@8.4.1: {} + undici@8.6.0: {} unhead@2.1.15: dependencies: @@ -29128,14 +29475,14 @@ snapshots: url-join@5.0.0: {} - url-loader@4.1.1(file-loader@6.2.0(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)): + url-loader@4.1.1(file-loader@6.2.0(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)): dependencies: loader-utils: 2.0.4 mime-types: 2.1.35 schema-utils: 3.3.0 - webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16) optionalDependencies: - file-loader: 6.2.0(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)) + file-loader: 6.2.0(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)) use-callback-ref@1.3.3(@types/react@19.2.17)(react@19.2.7): dependencies: @@ -29154,11 +29501,11 @@ snapshots: dequal: 2.0.3 react: 19.2.7 - use-intl@4.13.0(react@19.2.7): + use-intl@4.13.1(react@19.2.7): dependencies: '@formatjs/fast-memoize': 3.1.0 '@schummar/icu-type-parser': 1.21.5 - icu-minify: 4.13.0 + icu-minify: 4.13.1 intl-messageformat: 11.1.1 react: 19.2.7 @@ -29258,38 +29605,38 @@ snapshots: dependencies: global: 4.4.0 - vite-tsconfig-paths@6.1.1(typescript@6.0.3)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.15))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)): + vite-tsconfig-paths@6.1.1(typescript@6.0.3)(vite@8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.3(typescript@6.0.3) - vite: 8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.15))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0) + vite: 8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color - typescript - vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.15))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0): + vite@8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.15 - rolldown: 1.0.3 + postcss: 8.5.16 + rolldown: 1.1.4 tinyglobby: 0.2.17 optionalDependencies: '@types/node': 24.13.2 - esbuild: 0.28.0 + esbuild: 0.28.1 fsevents: 2.3.3 jiti: 2.7.0 sass: 1.101.0 - sugarss: 5.0.0(postcss@8.5.15) + sugarss: 5.0.0(postcss@8.5.16) terser: 5.46.1 tsx: 4.21.0 yaml: 2.9.0 - vitest@4.1.9(@opentelemetry/api@1.9.0)(@types/node@24.13.2)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.15))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)): + vitest@4.1.9(@opentelemetry/api@1.9.0)(@types/node@24.13.2)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.9 - '@vitest/mocker': 4.1.9(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.15))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)) + '@vitest/mocker': 4.1.9(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.9 '@vitest/runner': 4.1.9 '@vitest/snapshot': 4.1.9 @@ -29306,7 +29653,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.16(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.15))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0) + vite: 8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 @@ -29376,7 +29723,7 @@ snapshots: - bufferutil - utf-8-validate - webpack-dev-middleware@7.4.5(tslib@2.8.1)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)): + webpack-dev-middleware@7.4.5(tslib@2.8.1)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)): dependencies: colorette: 2.0.20 memfs: 4.57.6(tslib@2.8.1) @@ -29385,11 +29732,11 @@ snapshots: range-parser: 1.2.1 schema-utils: 4.3.3 optionalDependencies: - webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16) transitivePeerDependencies: - tslib - webpack-dev-server@5.2.4(tslib@2.8.1)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)): + webpack-dev-server@5.2.4(tslib@2.8.1)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -29417,10 +29764,10 @@ snapshots: serve-index: 1.9.2 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)) + webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)) ws: 8.21.0 optionalDependencies: - webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16) transitivePeerDependencies: - bufferutil - debug @@ -29442,7 +29789,7 @@ snapshots: webpack-sources@3.5.0: {} - webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(@swc/html@1.15.40)(esbuild@0.28.0)(lightningcss@1.32.0)(postcss@8.5.15): + webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(@swc/html@1.15.40)(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.16): dependencies: '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 @@ -29464,7 +29811,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.3 - terser-webpack-plugin: 5.6.0(@swc/core@1.15.3(@swc/helpers@0.5.15))(@swc/html@1.15.40)(esbuild@0.28.0)(lightningcss@1.32.0)(postcss@8.5.15)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)) + terser-webpack-plugin: 5.6.0(@swc/core@1.15.3(@swc/helpers@0.5.15))(@swc/html@1.15.40)(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.16)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)) watchpack: 2.5.1 webpack-sources: 3.5.0 transitivePeerDependencies: @@ -29481,7 +29828,7 @@ snapshots: - postcss - uglify-js - webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15): + webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16): dependencies: '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 @@ -29503,7 +29850,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.3 - terser-webpack-plugin: 5.6.0(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)) + terser-webpack-plugin: 5.6.0(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)) watchpack: 2.5.1 webpack-sources: 3.5.0 transitivePeerDependencies: @@ -29520,7 +29867,7 @@ snapshots: - postcss - uglify-js - webpack@5.107.1(esbuild@0.28.0)(postcss@8.5.15): + webpack@5.107.1(esbuild@0.28.1)(postcss@8.5.16): dependencies: '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 @@ -29542,7 +29889,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.3 - terser-webpack-plugin: 5.6.0(esbuild@0.28.0)(postcss@8.5.15)(webpack@5.107.1(esbuild@0.28.0)(postcss@8.5.15)) + terser-webpack-plugin: 5.6.0(esbuild@0.28.1)(postcss@8.5.16)(webpack@5.107.1(esbuild@0.28.1)(postcss@8.5.16)) watchpack: 2.5.1 webpack-sources: 3.5.0 transitivePeerDependencies: @@ -29559,7 +29906,7 @@ snapshots: - postcss - uglify-js - webpackbar@7.0.0(@rspack/core@1.7.11(@swc/helpers@0.5.15))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.0)(postcss@8.5.15)): + webpackbar@7.0.0(@rspack/core@1.7.11(@swc/helpers@0.5.15))(webpack@5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)): dependencies: ansis: 3.17.0 consola: 3.4.2 @@ -29567,7 +29914,7 @@ snapshots: std-env: 3.10.0 optionalDependencies: '@rspack/core': 1.7.11(@swc/helpers@0.5.15) - webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.0)(html-minifier-terser@7.2.0)(postcss@8.5.15) + webpack: 5.107.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16) websocket-driver@0.7.5: dependencies: @@ -29681,8 +30028,6 @@ snapshots: dependencies: is-wsl: 3.1.1 - xcase@2.0.1: {} - xdg-basedir@5.1.0: {} xml-js@1.6.11: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 24ff4891f..c33da635f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -14,44 +14,45 @@ catalog: "@dnd-kit/utilities": ^3.2.2 "@drizzle-team/brocli": ^0.12.0 "@extractus/feed-extractor": 7.2.1 - '@gfazioli/mantine-onboarding-tour': ^4.0.5 + "@gfazioli/mantine-onboarding-tour": ^4.0.6 "@gitbeaker/rest": ^43.8.0 "@homarr/gridstack": ^1.12.0 "@homarr/node-unifi": ^2.6.0 - "@immich/sdk": ^2.7.5 + "@immich/sdk": ^3.0.1 "@jellyfin/sdk": ^0.13.0 "@kubernetes/client-node": ^1.4.0 "@modelcontextprotocol/sdk": 1.29.0 - "@mantine/charts": ^9.3.2 - "@mantine/colors-generator": ^9.3.2 - "@mantine/core": ^9.3.2 - "@mantine/dates": ^9.3.2 - "@mantine/dropzone": ^9.3.2 - "@mantine/form": ^9.3.2 - "@mantine/hooks": ^9.3.2 - "@mantine/modals": ^9.3.2 - "@mantine/notifications": ^9.3.2 - "@mantine/spotlight": ^9.3.2 - "@mantine/tiptap": ^9.3.2 - "@next/eslint-plugin-next": 16.2.9 + "@mantine/charts": ^9.4.1 + "@mantine/colors-generator": ^9.4.1 + "@mantine/core": ^9.4.1 + "@mantine/dates": ^9.4.1 + "@mantine/dropzone": ^9.4.1 + "@mantine/form": ^9.4.1 + "@mantine/hooks": ^9.4.1 + "@mantine/modals": ^9.4.1 + "@mantine/notifications": ^9.4.1 + "@mantine/spotlight": ^9.4.1 + "@mantine/tiptap": ^9.4.1 + "@next/eslint-plugin-next": 16.2.10 "@octokit/auth-app": ^8.2.0 "@paralleldrive/cuid2": ^3.3.0 - "@playwright/test": ^1.61.0 + "@playwright/test": ^1.61.1 "@semantic-release/changelog": ^6.0.3 "@semantic-release/commit-analyzer": ^13.0.1 "@semantic-release/git": ^10.0.1 - "@semantic-release/github": ^12.0.8 + "@semantic-release/github": ^12.0.9 "@semantic-release/npm": ^13.1.5 "@semantic-release/release-notes-generator": ^14.1.1 "@t3-oss/env-nextjs": ^0.13.11 "@tabler/icons-react": ^3.44.0 - "@tanstack/query-persist-client-core": ^5.101.0 - "@tanstack/react-query": ^5.101.0 - "@tanstack/react-query-devtools": ^5.101.0 - "@tanstack/react-query-next-experimental": ^5.101.0 - "@testcontainers/mysql": ^12.0.3 - "@testcontainers/postgresql": ^12.0.3 - "@testcontainers/redis": ^12.0.3 + "@tanstack/query-async-storage-persister": ^5.101.2 + "@tanstack/react-query": ^5.101.2 + "@tanstack/react-query-devtools": ^5.101.2 + "@tanstack/react-query-next-experimental": ^5.101.2 + "@tanstack/react-query-persist-client": ^5.101.2 + "@testcontainers/mysql": ^12.0.4 + "@testcontainers/postgresql": ^12.0.4 + "@testcontainers/redis": ^12.0.4 "@tiptap/extension-color": 3.27.1 "@tiptap/extension-highlight": 3.27.1 "@tiptap/extension-image": 3.27.1 @@ -68,12 +69,12 @@ catalog: "@tiptap/extension-underline": 3.27.1 "@tiptap/react": ^3.27.1 "@tiptap/starter-kit": ^3.27.1 - "@trpc/client": ^11.17.0 - "@trpc/next": ^11.17.0 - "@trpc/react-query": ^11.17.0 - "@trpc/server": ^11.17.0 - "@trpc/tanstack-react-query": ^11.17.0 - "@turbo/gen": ^2.9.18 + "@trpc/client": ^11.18.0 + "@trpc/next": ^11.18.0 + "@trpc/react-query": ^11.18.0 + "@trpc/server": ^11.18.0 + "@trpc/tanstack-react-query": ^11.18.0 + "@turbo/gen": ^2.10.2 "@types/adm-zip": 0.5.8 "@types/bcrypt": 6.0.0 "@types/better-sqlite3": 7.6.13 @@ -88,29 +89,29 @@ catalog: "@types/prismjs": ^1.26.6 "@types/react": 19.2.17 "@types/react-dom": 19.2.3 - "@scalar/api-reference-react": ^0.9.47 + "@scalar/api-reference-react": ^0.9.51 "@types/video.js": ^7.3.58 "@types/ws": ^8.18.1 "@types/xml2js": ^0.4.14 - posthog-js: ^1.391.2 - posthog-node: ^4.18.0 - "@vitejs/plugin-react": ^6.0.2 + posthog-js: ^1.396.4 + posthog-node: ^5.39.2 + "@vitejs/plugin-react": ^6.0.3 "@vitest/coverage-v8": ^4.1.9 "@vitest/ui": ^4.1.9 "@xterm/addon-canvas": ^0.7.0 "@xterm/addon-fit": 0.11.0 "@xterm/xterm": ^6.0.0 - adm-zip: 0.5.17 + adm-zip: 0.5.18 jszip: ^3.10.1 sql.js: ^1.14.1 babel-plugin-react-compiler: ^1.0.0 bcrypt: ^6.0.0 - better-sqlite3: ^12.10.0 + better-sqlite3: ^12.11.1 chroma-js: ^3.2.0 clsx: ^2.1.1 concurrently: ^10.0.3 - conventional-changelog-conventionalcommits: ^9.3.1 - cookie: 1.1.1 + conventional-changelog-conventionalcommits: ^10.2.0 + cookie: 2.0.1 cookies: ^0.9.1 croner: 10.0.1 cross-env: ^10.1.0 @@ -118,7 +119,7 @@ catalog: dayjs: ^1.11.21 deepmerge: 4.3.1 dns-caching: ^0.2.9 - dockerode: ^5.0.0 + dockerode: ^5.0.1 dotenv: ^17.4.2 dotenv-cli: ^11.0.0 drizzle-kit: ^0.31.10 @@ -126,7 +127,7 @@ catalog: drizzle-zod: ^0.8.3 esbuild: ^0.28.0 fast-xml-parser: ^5.9.3 - fastify: ^5.8.5 + fastify: ^5.9.0 flag-icons: ^7.5.0 glob: ^13.0.6 ical.js: ^2.2.1 @@ -138,20 +139,20 @@ catalog: jsonpath-plus: ^10.4.0 ldapts: 8.1.8 lodash.clonedeep: ^4.5.0 - lucide-react: ^1.21.0 + lucide-react: ^1.23.0 "mantine-contextmenu": ^9.3.1 mantine-datatable: ^9.3.1 mantine-react-table: npm:mantine-react-table-open@9.0.3 mcp-handler: ^1.1.0 maria2: ^0.4.1 mysql2: 3.22.5 - next: 16.2.9 + next: 16.2.10 next-auth: 5.0.0-beta.31 - next-intl: 4.13.0 + next-intl: 4.13.1 node-loader: ^2.1.0 octokit: ^5.0.5 - oxfmt: ^0.55.0 - oxlint: ^1.70.0 + oxfmt: ^0.57.0 + oxlint: ^1.72.0 pg: ^8.22.0 postcss-preset-mantine: ^1.18.0 prismjs: ^1.30.0 @@ -163,22 +164,22 @@ catalog: react-markdown: ^10.1.0 react-quill-new: ^3.8.3 react-simple-code-editor: ^0.14.1 - recharts: ^3.8.1 + recharts: ^3.9.1 sass: ^1.101.0 semantic-release: ^25.0.5 semver-parser: ^4.2.1 superjson: 2.2.6 svgson: ^5.3.1 - testcontainers: ^12.0.3 + testcontainers: ^12.0.4 timezones-ical-library: ^2.2.1 trpc-to-mcp: ^1.3.2 trpc-to-openapi: ^3.3.0 - tsdav: ^2.2.2 + tsdav: ^2.3.0 tsx: 4.20.4 - turbo: ^2.9.18 + turbo: ^2.10.2 typescript: ^6.0.3 - vite: ^8.0.16 - undici: 7.27.2 + vite: ^8.1.2 + undici: 7.28.0 use-deep-compare-effect: ^1.8.1 video.js: ^8.23.9 vite-tsconfig-paths: ^6.1.1 @@ -190,6 +191,7 @@ catalog: zod: ^4.4.3 zod-form-data: ^3.0.2 zod-validation-error: ^5.0.0 + catalogMode: strict nodeLinker: hoisted strictPeerDependencies: false @@ -199,33 +201,39 @@ minimumReleaseAge: 4320 minimumReleaseAgeExclude: - next - "@next/*" + - "@immich/sdk@3.0.0" # Renovate security update: turbo@2.9.14 - turbo@2.9.14 # Renovate security update: form-data@4.0.6 - form-data@4.0.6 + # Renovate security update: undici@7.28.0 + - undici@7.28.0 + # Renovate security update: esbuild@0.28.1 + - esbuild@0.28.1 overrides: "@babel/helpers@<7.26.10": ">=7.29.7" "@babel/runtime@<7.26.10": ">=7.29.7" - "axios@>=1.0.0 <1.8.2": ">=1.18.0" - "brace-expansion@>=2.0.0 <=2.0.1": ">=5.0.6" - "brace-expansion@>=1.0.0 <=1.1.11": ">=5.0.6" - "esbuild@<=0.24.2": ">=0.28.0" + "axios@>=1.0.0 <1.8.2": ">=1.18.1" + "brace-expansion@>=2.0.0 <=2.0.1": ">=5.0.7" + "brace-expansion@>=1.0.0 <=1.1.11": ">=5.0.7" + "esbuild@<=0.24.2": ">=0.28.1" "form-data@>=4.0.0 <4.0.4": ">=4.0.6" - "hono@<4.6.5": ">=4.12.26" + "hono@<4.6.5": ">=4.12.27" "linkifyjs@<4.3.2": ">=4.3.3" - "nanoid@>=4.0.0 <5.0.9": ">=5.1.15" + "nanoid@>=4.0.0 <5.0.9": ">=5.1.16" "prismjs@<1.30.0": ">=1.30.0" - "proxmox-api>undici": "7.27.1" + "proxmox-api>undici": "7.28.0" "react-is": "^19.2.7" "rollup@>=4.0.0 <4.22.4": ">=4.62.2" "sha.js@<=2.4.11": ">=2.4.12" - "tar-fs@>=3.0.0 <3.0.9": ">=3.1.2" - "tar-fs@>=2.0.0 <2.1.3": ">=3.1.2" + "tar-fs@>=3.0.0 <3.0.9": ">=3.1.3" + "tar-fs@>=2.0.0 <2.1.3": ">=3.1.3" "tmp@<=0.2.3": ">=0.2.7" - "vite@>=5.0.0 <=5.4.18": ">=8.0.16" + "vite@>=5.0.0 <=5.4.18": ">=8.1.3" patchedDependencies: "@types/node-unifi": "patches/@types__node-unifi.patch" "trpc-to-openapi": "patches/trpc-to-openapi.patch" + allowUnusedPatches: true allowBuilds: "@tree-sitter-grammars/tree-sitter-yaml": true diff --git a/static-data/contributors.json b/static-data/contributors.json index 07d22132c..c576fb0d1 100644 --- a/static-data/contributors.json +++ b/static-data/contributors.json @@ -1 +1 @@ -[{"login":"ajnart","avatar_url":"https://avatars.githubusercontent.com/u/49837342?v=4"},{"login":"manuel-rw","avatar_url":"https://avatars.githubusercontent.com/u/30572287?v=4"},{"login":"Meierschlumpf","avatar_url":"https://avatars.githubusercontent.com/u/63781622?v=4"},{"login":"SeDemal","avatar_url":"https://avatars.githubusercontent.com/u/26098587?v=4"},{"login":"lammersbjorn","avatar_url":"https://avatars.githubusercontent.com/u/71191962?v=4"},{"login":"LarveyOfficial","avatar_url":"https://avatars.githubusercontent.com/u/39219859?v=4"},{"login":"c00ldude1oo","avatar_url":"https://avatars.githubusercontent.com/u/15677803?v=4"},{"login":"InterN0te","avatar_url":"https://avatars.githubusercontent.com/u/10882916?v=4"},{"login":"Aimsucks","avatar_url":"https://avatars.githubusercontent.com/u/1828486?v=4"},{"login":"JannesV","avatar_url":"https://avatars.githubusercontent.com/u/5984244?v=4"},{"login":"MauriceNino","avatar_url":"https://avatars.githubusercontent.com/u/18658092?v=4"},{"login":"hillaliy","avatar_url":"https://avatars.githubusercontent.com/u/950010?v=4"},{"login":"Skeeww","avatar_url":"https://avatars.githubusercontent.com/u/9639992?v=4"},{"login":"RichyHBM","avatar_url":"https://avatars.githubusercontent.com/u/5472275?v=4"},{"login":"Bonfire","avatar_url":"https://avatars.githubusercontent.com/u/5704760?v=4"},{"login":"wieli99","avatar_url":"https://avatars.githubusercontent.com/u/46794825?v=4"},{"login":"ishaanparlikar","avatar_url":"https://avatars.githubusercontent.com/u/43013132?v=4"},{"login":"Zareix","avatar_url":"https://avatars.githubusercontent.com/u/29818713?v=4"},{"login":"tuggan","avatar_url":"https://avatars.githubusercontent.com/u/4325608?v=4"},{"login":"Spillebulle","avatar_url":"https://avatars.githubusercontent.com/u/46653946?v=4"},{"login":"marektancak0106","avatar_url":"https://avatars.githubusercontent.com/u/25422667?v=4"},{"login":"spkesDE","avatar_url":"https://avatars.githubusercontent.com/u/1682314?v=4"},{"login":"j3lte","avatar_url":"https://avatars.githubusercontent.com/u/2557568?v=4"},{"login":"dslatt","avatar_url":"https://avatars.githubusercontent.com/u/5211001?v=4"},{"login":"Malong11-007","avatar_url":"https://avatars.githubusercontent.com/u/40298510?v=4"},{"login":"StefanB7","avatar_url":"https://avatars.githubusercontent.com/u/22056509?v=4"},{"login":"souravghosh01","avatar_url":"https://avatars.githubusercontent.com/u/37548139?v=4"},{"login":"SR3u","avatar_url":"https://avatars.githubusercontent.com/u/2380522?v=4"},{"login":"Rikpat","avatar_url":"https://avatars.githubusercontent.com/u/33869814?v=4"},{"login":"NormanJS","avatar_url":"https://avatars.githubusercontent.com/u/83409549?v=4"},{"login":"No-Maines-Land","avatar_url":"https://avatars.githubusercontent.com/u/20687725?v=4"},{"login":"stark1tty","avatar_url":"https://avatars.githubusercontent.com/u/69326376?v=4"},{"login":"Momcilo42","avatar_url":"https://avatars.githubusercontent.com/u/10118137?v=4"},{"login":"marius-arch","avatar_url":"https://avatars.githubusercontent.com/u/72494359?v=4"},{"login":"Lumilias","avatar_url":"https://avatars.githubusercontent.com/u/10852161?v=4"},{"login":"Havock94","avatar_url":"https://avatars.githubusercontent.com/u/7635248?v=4"},{"login":"Lollyde","avatar_url":"https://avatars.githubusercontent.com/u/11347524?v=4"},{"login":"krishnamuppaneni","avatar_url":"https://avatars.githubusercontent.com/u/4703366?v=4"},{"login":"devtobi","avatar_url":"https://avatars.githubusercontent.com/u/28538704?v=4"},{"login":"TyxTang","avatar_url":"https://avatars.githubusercontent.com/u/90897955?v=4"},{"login":"beanbag420","avatar_url":"https://avatars.githubusercontent.com/u/19630511?v=4"},{"login":"WillyJL","avatar_url":"https://avatars.githubusercontent.com/u/49810075?v=4"},{"login":"anonysoul","avatar_url":"https://avatars.githubusercontent.com/u/105715150?v=4"},{"login":"cospeedster","avatar_url":"https://avatars.githubusercontent.com/u/52749645?v=4"},{"login":"diederbert","avatar_url":"https://avatars.githubusercontent.com/u/162878798?v=4"},{"login":"digdilem","avatar_url":"https://avatars.githubusercontent.com/u/4052275?v=4"},{"login":"dinghino","avatar_url":"https://avatars.githubusercontent.com/u/14729459?v=4"},{"login":"gfomichev","avatar_url":"https://avatars.githubusercontent.com/u/1596146?v=4"},{"login":"gnattu","avatar_url":"https://avatars.githubusercontent.com/u/12995396?v=4"},{"login":"tuxpizza","avatar_url":"https://avatars.githubusercontent.com/u/84710786?v=4"},{"login":"xmarkurion","avatar_url":"https://avatars.githubusercontent.com/u/46654687?v=4"},{"login":"huanga","avatar_url":"https://avatars.githubusercontent.com/u/732970?v=4"},{"login":"taos15","avatar_url":"https://avatars.githubusercontent.com/u/122130728?v=4"},{"login":"arghyadipchak","avatar_url":"https://avatars.githubusercontent.com/u/32430374?v=4"},{"login":"AuthorShin","avatar_url":"https://avatars.githubusercontent.com/u/4959043?v=4"},{"login":"benniblot","avatar_url":"https://avatars.githubusercontent.com/u/47495914?v=4"},{"login":"bebsworthy","avatar_url":"https://avatars.githubusercontent.com/u/5266759?v=4"},{"login":"catrielmuller","avatar_url":"https://avatars.githubusercontent.com/u/2272323?v=4"},{"login":"Vysp3r","avatar_url":"https://avatars.githubusercontent.com/u/62019735?v=4"},{"login":"DR-Blogs","avatar_url":"https://avatars.githubusercontent.com/u/96393186?v=4"},{"login":"singular0","avatar_url":"https://avatars.githubusercontent.com/u/1925883?v=4"},{"login":"diogovalentte","avatar_url":"https://avatars.githubusercontent.com/u/49578155?v=4"},{"login":"codezninja","avatar_url":"https://avatars.githubusercontent.com/u/4400773?v=4"},{"login":"fabricionaweb","avatar_url":"https://avatars.githubusercontent.com/u/15933?v=4"},{"login":"nyok1912","avatar_url":"https://avatars.githubusercontent.com/u/32163949?v=4"},{"login":"bo3bdo","avatar_url":"https://avatars.githubusercontent.com/u/5271380?v=4"},{"login":"hskrtich","avatar_url":"https://avatars.githubusercontent.com/u/1214484?v=4"},{"login":"ishults","avatar_url":"https://avatars.githubusercontent.com/u/7275442?v=4"},{"login":"jelliuk","avatar_url":"https://avatars.githubusercontent.com/u/4790959?v=4"},{"login":"Moohan","avatar_url":"https://avatars.githubusercontent.com/u/5982260?v=4"},{"login":"tesar-tech","avatar_url":"https://avatars.githubusercontent.com/u/33880579?v=4"},{"login":"jbruell","avatar_url":"https://avatars.githubusercontent.com/u/31685172?v=4"},{"login":"JasonLeeB06","avatar_url":"https://avatars.githubusercontent.com/u/82266978?v=4"},{"login":"jeffersonraimon","avatar_url":"https://avatars.githubusercontent.com/u/80064475?v=4"},{"login":"jo-chemla","avatar_url":"https://avatars.githubusercontent.com/u/16822841?v=4"},{"login":"joser93","avatar_url":"https://avatars.githubusercontent.com/u/6912089?v=4"},{"login":"justijndepover","avatar_url":"https://avatars.githubusercontent.com/u/9008623?v=4"},{"login":"Aandree5","avatar_url":"https://avatars.githubusercontent.com/u/32734153?v=4"},{"login":"afonsojramos","avatar_url":"https://avatars.githubusercontent.com/u/19473034?v=4"},{"login":"mvanhorn","avatar_url":"https://avatars.githubusercontent.com/u/455140?v=4"},{"login":"jackwellerreal","avatar_url":"https://avatars.githubusercontent.com/u/97435840?v=4"},{"login":"Copilot","avatar_url":"https://avatars.githubusercontent.com/in/1143301?v=4"},{"login":"SergioChan","avatar_url":"https://avatars.githubusercontent.com/u/10103766?v=4"},{"login":"toineenzo","avatar_url":"https://avatars.githubusercontent.com/u/4921397?v=4"},{"login":"oben01","avatar_url":"https://avatars.githubusercontent.com/u/22369027?v=4"},{"login":"rezstje","avatar_url":"https://avatars.githubusercontent.com/u/45420315?v=4"},{"login":"castielwaverly","avatar_url":"https://avatars.githubusercontent.com/u/137005689?v=4"},{"login":"xoxyuxu","avatar_url":"https://avatars.githubusercontent.com/u/3543416?v=4"},{"login":"StrandedTurtle","avatar_url":"https://avatars.githubusercontent.com/u/132835255?v=4"},{"login":"MaienM","avatar_url":"https://avatars.githubusercontent.com/u/583129?v=4"},{"login":"kylegrieder","avatar_url":"https://avatars.githubusercontent.com/u/25424229?v=4"},{"login":"HeapReaper","avatar_url":"https://avatars.githubusercontent.com/u/31496522?v=4"},{"login":"lopsch","avatar_url":"https://avatars.githubusercontent.com/u/4073114?v=4"},{"login":"Gilmoursa","avatar_url":"https://avatars.githubusercontent.com/u/10321421?v=4"},{"login":"Lok3sh-kumar","avatar_url":"https://avatars.githubusercontent.com/u/110459168?v=4"},{"login":"Bricklou","avatar_url":"https://avatars.githubusercontent.com/u/15181236?v=4"},{"login":"mairas","avatar_url":"https://avatars.githubusercontent.com/u/407010?v=4"},{"login":"itzTheMeow","avatar_url":"https://avatars.githubusercontent.com/u/50887230?v=4"},{"login":"Nerdy-Technician","avatar_url":"https://avatars.githubusercontent.com/u/45691205?v=4"},{"login":"NicolasNewman","avatar_url":"https://avatars.githubusercontent.com/u/16135852?v=4"},{"login":"NikeLaosClericus","avatar_url":"https://avatars.githubusercontent.com/u/39810627?v=4"},{"login":"noahcroghan","avatar_url":"https://avatars.githubusercontent.com/u/27422774?v=4"},{"login":"ohmzi","avatar_url":"https://avatars.githubusercontent.com/u/6551272?v=4"},{"login":"Rhiz3K","avatar_url":"https://avatars.githubusercontent.com/u/33246262?v=4"},{"login":"saschabuehrle","avatar_url":"https://avatars.githubusercontent.com/u/47737812?v=4"},{"login":"CrazyWolf13","avatar_url":"https://avatars.githubusercontent.com/u/96661824?v=4"},{"login":"VIVAAN-DHAWAN","avatar_url":"https://avatars.githubusercontent.com/u/150339722?v=4"},{"login":"generalmat82","avatar_url":"https://avatars.githubusercontent.com/u/110256073?v=4"},{"login":"lthomsen","avatar_url":"https://avatars.githubusercontent.com/u/28560325?v=4"},{"login":"pitschi","avatar_url":"https://avatars.githubusercontent.com/u/6841565?v=4"},{"login":"sharpsalat","avatar_url":"https://avatars.githubusercontent.com/u/15666932?v=4"},{"login":"tech62","avatar_url":"https://avatars.githubusercontent.com/u/16237642?v=4"},{"login":"AartSchinkel","avatar_url":"https://avatars.githubusercontent.com/u/189675191?v=4"},{"login":"m0s4ik","avatar_url":"https://avatars.githubusercontent.com/u/26578395?v=4"},{"login":"ashtonbatty","avatar_url":"https://avatars.githubusercontent.com/u/230635?v=4"},{"login":"Oupsman","avatar_url":"https://avatars.githubusercontent.com/u/11260343?v=4"},{"login":"Bergasha","avatar_url":"https://avatars.githubusercontent.com/u/25044200?v=4"},{"login":"cethien","avatar_url":"https://avatars.githubusercontent.com/u/60463918?v=4"},{"login":"DZ945brH","avatar_url":"https://avatars.githubusercontent.com/u/42635243?v=4"},{"login":"dgshue","avatar_url":"https://avatars.githubusercontent.com/u/12952156?v=4"},{"login":"d-lindahl","avatar_url":"https://avatars.githubusercontent.com/u/690410?v=4"},{"login":"KartulUdus","avatar_url":"https://avatars.githubusercontent.com/u/23346150?v=4"},{"login":"ieugen","avatar_url":"https://avatars.githubusercontent.com/u/1050920?v=4"},{"login":"faugusto-oliveira","avatar_url":"https://avatars.githubusercontent.com/u/37384074?v=4"},{"login":"frankvHoof93","avatar_url":"https://avatars.githubusercontent.com/u/7346516?v=4"},{"login":"darbyjack","avatar_url":"https://avatars.githubusercontent.com/u/21014720?v=4"},{"login":"febrile42","avatar_url":"https://avatars.githubusercontent.com/u/14597122?v=4"},{"login":"Kaotic3","avatar_url":"https://avatars.githubusercontent.com/u/80522225?v=4"},{"login":"KelianB","avatar_url":"https://avatars.githubusercontent.com/u/11216155?v=4"},{"login":"hotrungnhan","avatar_url":"https://avatars.githubusercontent.com/u/40029631?v=4"},{"login":"leandroscardua","avatar_url":"https://avatars.githubusercontent.com/u/24698541?v=4"}] \ No newline at end of file +[{"login":"ajnart","avatar_url":"https://avatars.githubusercontent.com/u/49837342?v=4"},{"login":"manuel-rw","avatar_url":"https://avatars.githubusercontent.com/u/30572287?v=4"},{"login":"Meierschlumpf","avatar_url":"https://avatars.githubusercontent.com/u/63781622?v=4"},{"login":"SeDemal","avatar_url":"https://avatars.githubusercontent.com/u/26098587?v=4"},{"login":"lammersbjorn","avatar_url":"https://avatars.githubusercontent.com/u/71191962?v=4"},{"login":"LarveyOfficial","avatar_url":"https://avatars.githubusercontent.com/u/39219859?v=4"},{"login":"c00ldude1oo","avatar_url":"https://avatars.githubusercontent.com/u/15677803?v=4"},{"login":"InterN0te","avatar_url":"https://avatars.githubusercontent.com/u/10882916?v=4"},{"login":"Aimsucks","avatar_url":"https://avatars.githubusercontent.com/u/1828486?v=4"},{"login":"JannesV","avatar_url":"https://avatars.githubusercontent.com/u/5984244?v=4"},{"login":"MauriceNino","avatar_url":"https://avatars.githubusercontent.com/u/18658092?v=4"},{"login":"hillaliy","avatar_url":"https://avatars.githubusercontent.com/u/950010?v=4"},{"login":"Skeeww","avatar_url":"https://avatars.githubusercontent.com/u/9639992?v=4"},{"login":"RichyHBM","avatar_url":"https://avatars.githubusercontent.com/u/5472275?v=4"},{"login":"Bonfire","avatar_url":"https://avatars.githubusercontent.com/u/5704760?v=4"},{"login":"wieli99","avatar_url":"https://avatars.githubusercontent.com/u/46794825?v=4"},{"login":"ishaanparlikar","avatar_url":"https://avatars.githubusercontent.com/u/43013132?v=4"},{"login":"Zareix","avatar_url":"https://avatars.githubusercontent.com/u/29818713?v=4"},{"login":"tuggan","avatar_url":"https://avatars.githubusercontent.com/u/4325608?v=4"},{"login":"Spillebulle","avatar_url":"https://avatars.githubusercontent.com/u/46653946?v=4"},{"login":"marektancak0106","avatar_url":"https://avatars.githubusercontent.com/u/25422667?v=4"},{"login":"spkesDE","avatar_url":"https://avatars.githubusercontent.com/u/1682314?v=4"},{"login":"j3lte","avatar_url":"https://avatars.githubusercontent.com/u/2557568?v=4"},{"login":"dslatt","avatar_url":"https://avatars.githubusercontent.com/u/5211001?v=4"},{"login":"Malong11-007","avatar_url":"https://avatars.githubusercontent.com/u/40298510?v=4"},{"login":"StefanB7","avatar_url":"https://avatars.githubusercontent.com/u/22056509?v=4"},{"login":"souravghosh01","avatar_url":"https://avatars.githubusercontent.com/u/37548139?v=4"},{"login":"SR3u","avatar_url":"https://avatars.githubusercontent.com/u/2380522?v=4"},{"login":"Rikpat","avatar_url":"https://avatars.githubusercontent.com/u/33869814?v=4"},{"login":"NormanJS","avatar_url":"https://avatars.githubusercontent.com/u/83409549?v=4"},{"login":"No-Maines-Land","avatar_url":"https://avatars.githubusercontent.com/u/20687725?v=4"},{"login":"stark1tty","avatar_url":"https://avatars.githubusercontent.com/u/69326376?v=4"},{"login":"Momcilo42","avatar_url":"https://avatars.githubusercontent.com/u/10118137?v=4"},{"login":"marius-arch","avatar_url":"https://avatars.githubusercontent.com/u/72494359?v=4"},{"login":"Lumilias","avatar_url":"https://avatars.githubusercontent.com/u/10852161?v=4"},{"login":"Havock94","avatar_url":"https://avatars.githubusercontent.com/u/7635248?v=4"},{"login":"Lollyde","avatar_url":"https://avatars.githubusercontent.com/u/11347524?v=4"},{"login":"krishnamuppaneni","avatar_url":"https://avatars.githubusercontent.com/u/4703366?v=4"},{"login":"devtobi","avatar_url":"https://avatars.githubusercontent.com/u/28538704?v=4"},{"login":"TyxTang","avatar_url":"https://avatars.githubusercontent.com/u/90897955?v=4"},{"login":"beanbag420","avatar_url":"https://avatars.githubusercontent.com/u/19630511?v=4"},{"login":"WillyJL","avatar_url":"https://avatars.githubusercontent.com/u/49810075?v=4"},{"login":"anonysoul","avatar_url":"https://avatars.githubusercontent.com/u/105715150?v=4"},{"login":"cospeedster","avatar_url":"https://avatars.githubusercontent.com/u/52749645?v=4"},{"login":"diederbert","avatar_url":"https://avatars.githubusercontent.com/u/162878798?v=4"},{"login":"digdilem","avatar_url":"https://avatars.githubusercontent.com/u/4052275?v=4"},{"login":"dinghino","avatar_url":"https://avatars.githubusercontent.com/u/14729459?v=4"},{"login":"gfomichev","avatar_url":"https://avatars.githubusercontent.com/u/1596146?v=4"},{"login":"gnattu","avatar_url":"https://avatars.githubusercontent.com/u/12995396?v=4"},{"login":"tuxpizza","avatar_url":"https://avatars.githubusercontent.com/u/84710786?v=4"},{"login":"xmarkurion","avatar_url":"https://avatars.githubusercontent.com/u/46654687?v=4"},{"login":"huanga","avatar_url":"https://avatars.githubusercontent.com/u/732970?v=4"},{"login":"taos15","avatar_url":"https://avatars.githubusercontent.com/u/122130728?v=4"},{"login":"arghyadipchak","avatar_url":"https://avatars.githubusercontent.com/u/32430374?v=4"},{"login":"AuthorShin","avatar_url":"https://avatars.githubusercontent.com/u/4959043?v=4"},{"login":"benniblot","avatar_url":"https://avatars.githubusercontent.com/u/47495914?v=4"},{"login":"bebsworthy","avatar_url":"https://avatars.githubusercontent.com/u/5266759?v=4"},{"login":"catrielmuller","avatar_url":"https://avatars.githubusercontent.com/u/2272323?v=4"},{"login":"Vysp3r","avatar_url":"https://avatars.githubusercontent.com/u/62019735?v=4"},{"login":"DR-Blogs","avatar_url":"https://avatars.githubusercontent.com/u/96393186?v=4"},{"login":"singular0","avatar_url":"https://avatars.githubusercontent.com/u/1925883?v=4"},{"login":"diogovalentte","avatar_url":"https://avatars.githubusercontent.com/u/49578155?v=4"},{"login":"codezninja","avatar_url":"https://avatars.githubusercontent.com/u/4400773?v=4"},{"login":"fabricionaweb","avatar_url":"https://avatars.githubusercontent.com/u/15933?v=4"},{"login":"nyok1912","avatar_url":"https://avatars.githubusercontent.com/u/32163949?v=4"},{"login":"Hamad3bdulla","avatar_url":"https://avatars.githubusercontent.com/u/5271380?v=4"},{"login":"hskrtich","avatar_url":"https://avatars.githubusercontent.com/u/1214484?v=4"},{"login":"ishults","avatar_url":"https://avatars.githubusercontent.com/u/7275442?v=4"},{"login":"jelliuk","avatar_url":"https://avatars.githubusercontent.com/u/4790959?v=4"},{"login":"Moohan","avatar_url":"https://avatars.githubusercontent.com/u/5982260?v=4"},{"login":"tesar-tech","avatar_url":"https://avatars.githubusercontent.com/u/33880579?v=4"},{"login":"jbruell","avatar_url":"https://avatars.githubusercontent.com/u/31685172?v=4"},{"login":"JasonLeeB06","avatar_url":"https://avatars.githubusercontent.com/u/82266978?v=4"},{"login":"jeffersonraimon","avatar_url":"https://avatars.githubusercontent.com/u/80064475?v=4"},{"login":"jo-chemla","avatar_url":"https://avatars.githubusercontent.com/u/16822841?v=4"},{"login":"joser93","avatar_url":"https://avatars.githubusercontent.com/u/6912089?v=4"},{"login":"justijndepover","avatar_url":"https://avatars.githubusercontent.com/u/9008623?v=4"},{"login":"Aandree5","avatar_url":"https://avatars.githubusercontent.com/u/32734153?v=4"},{"login":"afonsojramos","avatar_url":"https://avatars.githubusercontent.com/u/19473034?v=4"},{"login":"mvanhorn","avatar_url":"https://avatars.githubusercontent.com/u/455140?v=4"},{"login":"jackwellerreal","avatar_url":"https://avatars.githubusercontent.com/u/97435840?v=4"},{"login":"Bergasha","avatar_url":"https://avatars.githubusercontent.com/u/25044200?v=4"},{"login":"Copilot","avatar_url":"https://avatars.githubusercontent.com/in/1143301?v=4"},{"login":"SergioChan","avatar_url":"https://avatars.githubusercontent.com/u/10103766?v=4"},{"login":"toineenzo","avatar_url":"https://avatars.githubusercontent.com/u/4921397?v=4"},{"login":"oben01","avatar_url":"https://avatars.githubusercontent.com/u/22369027?v=4"},{"login":"rezstje","avatar_url":"https://avatars.githubusercontent.com/u/45420315?v=4"},{"login":"darbyjack","avatar_url":"https://avatars.githubusercontent.com/u/21014720?v=4"},{"login":"castielwaverly","avatar_url":"https://avatars.githubusercontent.com/u/137005689?v=4"},{"login":"xoxyuxu","avatar_url":"https://avatars.githubusercontent.com/u/3543416?v=4"},{"login":"tvervest","avatar_url":"https://avatars.githubusercontent.com/u/5818471?v=4"},{"login":"StrandedTurtle","avatar_url":"https://avatars.githubusercontent.com/u/132835255?v=4"},{"login":"MaienM","avatar_url":"https://avatars.githubusercontent.com/u/583129?v=4"},{"login":"kylegrieder","avatar_url":"https://avatars.githubusercontent.com/u/25424229?v=4"},{"login":"HeapReaper","avatar_url":"https://avatars.githubusercontent.com/u/31496522?v=4"},{"login":"lopsch","avatar_url":"https://avatars.githubusercontent.com/u/4073114?v=4"},{"login":"Gilmoursa","avatar_url":"https://avatars.githubusercontent.com/u/10321421?v=4"},{"login":"AartSchinkel","avatar_url":"https://avatars.githubusercontent.com/u/189675191?v=4"},{"login":"mairas","avatar_url":"https://avatars.githubusercontent.com/u/407010?v=4"},{"login":"itzTheMeow","avatar_url":"https://avatars.githubusercontent.com/u/50887230?v=4"},{"login":"Nerdy-Technician","avatar_url":"https://avatars.githubusercontent.com/u/45691205?v=4"},{"login":"NicolasNewman","avatar_url":"https://avatars.githubusercontent.com/u/16135852?v=4"},{"login":"NikeLaosClericus","avatar_url":"https://avatars.githubusercontent.com/u/39810627?v=4"},{"login":"noahcroghan","avatar_url":"https://avatars.githubusercontent.com/u/27422774?v=4"},{"login":"ohmzi","avatar_url":"https://avatars.githubusercontent.com/u/6551272?v=4"},{"login":"Rhiz3K","avatar_url":"https://avatars.githubusercontent.com/u/33246262?v=4"},{"login":"saschabuehrle","avatar_url":"https://avatars.githubusercontent.com/u/47737812?v=4"},{"login":"CrazyWolf13","avatar_url":"https://avatars.githubusercontent.com/u/96661824?v=4"},{"login":"VIVAAN-DHAWAN","avatar_url":"https://avatars.githubusercontent.com/u/150339722?v=4"},{"login":"euduardo","avatar_url":"https://avatars.githubusercontent.com/u/22860571?v=4"},{"login":"generalmat82","avatar_url":"https://avatars.githubusercontent.com/u/110256073?v=4"},{"login":"lthomsen","avatar_url":"https://avatars.githubusercontent.com/u/28560325?v=4"},{"login":"pitschi","avatar_url":"https://avatars.githubusercontent.com/u/6841565?v=4"},{"login":"sharpsalat","avatar_url":"https://avatars.githubusercontent.com/u/15666932?v=4"},{"login":"tech62","avatar_url":"https://avatars.githubusercontent.com/u/16237642?v=4"},{"login":"m0s4ik","avatar_url":"https://avatars.githubusercontent.com/u/26578395?v=4"},{"login":"ashtonbatty","avatar_url":"https://avatars.githubusercontent.com/u/230635?v=4"},{"login":"Oupsman","avatar_url":"https://avatars.githubusercontent.com/u/11260343?v=4"},{"login":"cethien","avatar_url":"https://avatars.githubusercontent.com/u/60463918?v=4"},{"login":"DZ945brH","avatar_url":"https://avatars.githubusercontent.com/u/42635243?v=4"},{"login":"dgshue","avatar_url":"https://avatars.githubusercontent.com/u/12952156?v=4"},{"login":"d-lindahl","avatar_url":"https://avatars.githubusercontent.com/u/690410?v=4"},{"login":"KartulUdus","avatar_url":"https://avatars.githubusercontent.com/u/23346150?v=4"},{"login":"ieugen","avatar_url":"https://avatars.githubusercontent.com/u/1050920?v=4"},{"login":"faugusto-oliveira","avatar_url":"https://avatars.githubusercontent.com/u/37384074?v=4"},{"login":"frankvHoof93","avatar_url":"https://avatars.githubusercontent.com/u/7346516?v=4"},{"login":"febrile42","avatar_url":"https://avatars.githubusercontent.com/u/14597122?v=4"},{"login":"mrjoshuap","avatar_url":"https://avatars.githubusercontent.com/u/5722442?v=4"},{"login":"Kaotic3","avatar_url":"https://avatars.githubusercontent.com/u/80522225?v=4"},{"login":"KelianB","avatar_url":"https://avatars.githubusercontent.com/u/11216155?v=4"},{"login":"hotrungnhan","avatar_url":"https://avatars.githubusercontent.com/u/40029631?v=4"},{"login":"leandroscardua","avatar_url":"https://avatars.githubusercontent.com/u/24698541?v=4"},{"login":"Lok3sh-kumar","avatar_url":"https://avatars.githubusercontent.com/u/110459168?v=4"},{"login":"Bricklou","avatar_url":"https://avatars.githubusercontent.com/u/15181236?v=4"}] \ No newline at end of file diff --git a/static-data/translators.json b/static-data/translators.json index 4d0b1456d..74c6a7689 100644 --- a/static-data/translators.json +++ b/static-data/translators.json @@ -1 +1 @@ -[{"username":"carlchina","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/6697/medium/d22bbe7797bbeb30dbdc73a5648d329a_default.png"},{"username":"zielmann","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/7795/medium/ad22b8b8d5eb33e4154d53a454c862fd_default.png"},{"username":"Thalyn","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12494028/medium/5faf5acc945a874ed116c4567edaa09f_default.png"},{"username":"magnushj","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12503488/medium/68f32e33ea7b891960055c9f831f7344_default.png"},{"username":"almontegil","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12580457/medium/f4136cacbdfdb4c28ae7f85dc5f840db_default.png"},{"username":"andibing","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12664938/medium/b8be63e4dcb2e791ced1ffc9e3a049a5.jpg"},{"username":"gronare","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12680911/medium/c6eb14d1504156f34a158e507f73aa3d_default.png"},{"username":"SmartPhoneLover","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12701640/medium/ec95486662ec875cda080e778c3ff702.jpg"},{"username":"marcelotk_15","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12773227/medium/ecf6bea16ab0c873c131df2154ee9718_default.png"},{"username":"S3OD177","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13051544/medium/11dd990837f61f7e8f7d36b041a7fddb_default.png"},{"username":"marcg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13153301/medium/ebcf60fbf27deb148ec40a659566c6ff_default.png"},{"username":"BeersTeddy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13185230/medium/2f1f4e1effe74a23422b195cbefb2a95_default.png"},{"username":"jamesmcmahon0","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13245578/medium/586aa873b4abddbd9abc6f3de99ab70e.jpeg"},{"username":"b1nar10","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13286752/medium/aaa1f083e04c572f953fde4563171e4a.jpeg"},{"username":"vannCN","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13330448/medium/9b8a9ee3611e51e951e22d5fd4eb7d8d.jpg"},{"username":"binswm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13343482/medium/45dde0e6097b9b72705d2eba9dbbc276_default.png"},{"username":"SkewRam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13496556/medium/188f5c2deb7938eda51eb786cc4539ca.jpeg"},{"username":"raphcatarino","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13547726/medium/b003511e67df13a4b4b5689488fa8099.jpg"},{"username":"shillos5","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13600557/medium/b17650b2b84f829d54fa548c44c1ab27.jpeg"},{"username":"wolong98","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13641407/medium/f4634edc58c7857a357e5293543c15cf.jpg"},{"username":"spair0039","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14012333/medium/72430e96027c09c19141cac38eae4617.png"},{"username":"MajorMarcin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14072183/medium/0a197e64bb92b6ea94cecbb607512c45.jpg"},{"username":"KosmoMoustache","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14103535/medium/51c438cdd9f86e8811c75f4869de479c.png"},{"username":"Soochaehwa","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14118689/medium/496a1ce63111547bf455a1e0a7ac75f1_default.png"},{"username":"garryfield","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14187035/medium/520c964706676045f6eeec1b08edc880.jpg"},{"username":"droidenko","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14444264/medium/4aa3a8e824f72bc9e5ec0d8de307095e.jpeg"},{"username":"maathis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14489950/medium/6eeddf13a757ee35f78f1763f94d95e1.jpeg"},{"username":"bukvam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14546502/medium/a819dc40a13c57395a17dc46e847d3ce.jpg"},{"username":"gm.cinalli","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14670666/medium/2d466a0fdbda40764526be86c97c0ab4.jpeg"},{"username":"antoine2tt","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14722148/medium/f88d926900862dd59007ea4b3419cb9d.png"},{"username":"cretzen","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14799754/medium/3ba5c1805ec20c000d5810c07985afe9.jpg"},{"username":"ktKongTong","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14817246/medium/97cfc3c028dbdaf85ebd1102da71e58c.jpeg"},{"username":"marinkaberg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14861042/medium/2d5c4e62613f03082f3e645fa92efd59.jpeg"},{"username":"djismgaming","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14934947/medium/f5a8570713c34ab0f7d5405d105e2a9a.jpeg"},{"username":"f1refa11","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14949159/medium/fd2ae63b8eb4462200ba96abf943c1b9.png"},{"username":"jeffersonraimon","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15057621/medium/6dff469563860e2618bc9d45646d0ab4.png"},{"username":"giop98","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15057987/medium/b8a4825d3fc39fc662f35ee258db4b2d.jpeg"},{"username":"tomislav.kraljevic","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15118857/medium/e133f1061cc92850b854d05d8faaeafd.png"},{"username":"Walkx","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15202182/medium/5c37361ae45aeed487b34582c1f7ca37.png"},{"username":"andrea.rosso","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15246318/medium/11f3f5ef44ec7f55b6f143090e208704_default.png"},{"username":"ajnart","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15270132/medium/a0f107a463c8910ee96bc2fa843a17e3.jpeg"},{"username":"Witchling","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15282238/medium/9e196b6783415b10ca571b7673c2ef33.jpg"},{"username":"guineuu","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15292058/medium/22fbb758bda3b7805d50bf21d38f2c20.jpeg"},{"username":"Bulgus","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15304568/medium/0e8787e5ceb02ed5c96a514d0068ae87.jpg"},{"username":"Narno","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15314996/medium/a6ba093f24884250a1bf6c6f2ca50f1b_default.png"},{"username":"BySempron","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15315986/medium/e3d22d7b1423c6823a9f36d595ed4bdb.png"},{"username":"jonathan.berglin.work","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15359236/medium/95930b2093db13b76179782f7322c5d5.png"},{"username":"icdmkg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15405504/medium/f334d16cde1ac4ffb935b440d03465af.gif"},{"username":"irithys","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15405614/medium/3086461c47cce0a0c031925e5f943412.png"},{"username":"JannesV","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15419912/medium/a7809eb4b817d7c49b62cf10ae86b950.png"},{"username":"benniblot","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15419914/medium/8086b1573d31f152ba41949b3a172445.jpeg"},{"username":"pacjo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15419916/medium/3cbeddbd7bc01faafb5a3bf47bba915b_default.png"},{"username":"Payou6994","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15419934/medium/37c9b9b17dfb578404c1c1ddb73ba7a8.png"},{"username":"fzibi21","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15419976/medium/0da688450358e0290a7b7359cc1f7328.png"},{"username":"WowMurdock","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15420118/medium/de19576fccb495b6dfe4c4c04a56b834.png"},{"username":"hbooo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15420120/medium/6c7c7f8db785061356ebb03d044d3329.jpeg"},{"username":"Manicraft1001","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15420178/medium/50ec94563a06a9f74f33bd09f01eed4d.jpg"},{"username":"Void123","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15420354/medium/86929d44df92a00f9fe900a985c196df_default.png"},{"username":"R4cc","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15422606/medium/a390979662b84694f59de30bdb732141.jpeg"},{"username":"fabricionaweb","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15425808/medium/a9354142e7ae5152c144409d55fda551_default.png"},{"username":"JokeOfDead","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15426890/medium/a383eac9365e9de64fd4ab1d6fd0cb95.jpeg"},{"username":"hkz","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15427174/medium/c88acefb0d7306e1f7470e872029fb39_default.png"},{"username":"Steken","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15428516/medium/d5aea5653c769c3a523182bdb60d1664.png"},{"username":"flar.anton","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15428592/medium/b01fdb365d892e9f811f77fcb50a80a2.jpeg"},{"username":"Bon","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15433542/medium/5397da4dfc821f20b6ac14fe0c514e9a.jpeg"},{"username":"bfkadan","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15434162/medium/4f9a0b43cfe3acaea60124c14ba7f44a.png"},{"username":"BunnySweety","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15434662/medium/f0ef200a6a0dcf0e1d0e9ecd4148f560_default.png"},{"username":"HRKings","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15436168/medium/69a31e461d38549f01864e7ef10d642e.png"},{"username":"wiston81","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15439078/medium/10f292c8d12a7c21a44b54495fa4a3d8.jpeg"},{"username":"qaz0911","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15440860/medium/3d3e50ee388c72dc4bf7a771761f2d89_default.png"},{"username":"qqyule","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15441462/medium/7a3cdf82710ffb5d8f388bc0bd010665.png"},{"username":"Bims0n","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15445560/medium/603220b603eeb3367e0f0d3fa675247c.jpg"},{"username":"TariqDaCoder","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15446228/medium/50b0f4040112bbd67690b769477398e5_default.png"},{"username":"Anarchon","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15449644/medium/e925e1f3e3ffbf0f982391ce263a1a28.jpeg"},{"username":"Ashun","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15453020/medium/ccdcf51c73d6aae40751bb30beee1915_default.png"},{"username":"sebekmartin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15454038/medium/bcfb44598cdfd1d7cd4eb35812538962.jpeg"},{"username":"RagnarGraves","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15459882/medium/143d5af850c1154070a218bea124e9cb_default.png"},{"username":"PrtmPhlp","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15462414/medium/b80db55e9de301432dcd1f8c8b24fd49_default.png"},{"username":"DooYoo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15470768/medium/2a18cf4be67094724b508c9e1e698a21_default.png"},{"username":"espentruls","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15486092/medium/8e38afc3a4ff669226a0cfd3e420ff3a.jpeg"},{"username":"frisco82","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15486922/medium/4e1c5d4189b42508e660daa3c1c25b2a.jpeg"},{"username":"lupineDK","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15491798/medium/461bd501e8371c062bf29ea171aedd36_default.png"},{"username":"hillaliy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15492732/medium/0bae17b421604892d888e3fc70cf0587.jpeg"},{"username":"MarcOrfilaCarreras","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15501072/medium/ea52b26c3c6f21e4931e38e3ce3f3d6e.png"},{"username":"robertbridda","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15507822/medium/a368c2e30411bb2da9b49290084191f3.png"},{"username":"MKoniuszko","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15518090/medium/87605434fcc839f6763ab07c50f6d232.jpeg"},{"username":"HeroSizy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15518710/medium/4e79c0e98cbeb536dd961e656331b509.png"},{"username":"dwt136","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15520022/medium/554422503f2baea43ace85facb4546fb_default.png"},{"username":"asifthewebguy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15526719/medium/b18931dd0c800d725048bd440646198b_default.png"},{"username":"eiloogs","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15545537/medium/f290a2f1190983530a9b76b2e858a609.gif"},{"username":"_vytdv","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15547289/medium/60d8644cc8ad6f11a92ccea4a14cd098_default.png"},{"username":"crendasien","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15554645/medium/598ab1d4aaf6b8dccd5ba16be92da7b9.jpeg"},{"username":"edxo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15573823/medium/8a565b732a75a77f840dd123cdb30bf4.png"},{"username":"somerlev","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15617065/medium/f4b13513e311ec902d90b2f718412c55.jpg"},{"username":"kid1412621","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15643771/medium/dd455e32de652fa88e6fd97598bdffa7.png"},{"username":"suming","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15644717/medium/244159dfe10fa03436205506f80c9e25.png"},{"username":"nick.gher","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15647517/medium/5374a2c6fef60a3fbf0edf86b997c351_default.png"},{"username":"DataCat","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15650315/medium/ce7c1365adf35c5d490d77500a4607fb_default.png"},{"username":"dizo89","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15658375/medium/0ca745e5017d491fe1b22b0239904de8.jpeg"},{"username":"tee_noodle","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15674577/medium/804d9cf06f5196026acb6436b809d0da_default.png"},{"username":"Marty88","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15674593/medium/492b1509d52bd2809dea768121217125.jpeg"},{"username":"Spillebulle","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15677023/medium/20b98bb85062e07afe0e63618f8a5feb.PNG"},{"username":"petitmewen","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15685239/medium/15de9b62d2e0bc25013435f1784bbcc1.jpeg"},{"username":"NoProsNoNoobs","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15687709/medium/ae8f65fafeb8bcf74dcb8871bbe46461.png"},{"username":"y.gybson","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15690777/medium/b5cb5d2d5768904ff6586c727e3a6c77.jpeg"},{"username":"inside90","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15704947/medium/c1355fcb30dd76f8e39d98d1d49f1c52.png"},{"username":"RJSkudra","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15709853/medium/c3abf2774913dc4e81fb261d36d7668c.png"},{"username":"binge203","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15713937/medium/77c1cfa4314673db80e6881fd9f64668.gif"},{"username":"Mailootje","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15714337/medium/743c3bc4ab1989966a375eeeec83d8b8.jpeg"},{"username":"tim-wiegers","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15719805/medium/08a96f064813350661cd6b20bf3d7d99.png"},{"username":"GkhnG","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15722911/medium/71a027caec489ef6ce82bcf1888329d0_default.png"},{"username":"Zoen-Millo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15760967/medium/9e956f11adc5b34f5636268b5c485dbf.jpg"},{"username":"HooinKyoma","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15792897/medium/9489f0a9b368e0e827ae758b740a2eed.jpeg"},{"username":"wathergeen","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15794495/medium/a83b7fc80d180669ebfd6deb3544cfd0.png"},{"username":"MoeToo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15818233/medium/d35cd6953717706eaf20f6c143c62947.png"},{"username":"itodouble","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15836233/medium/e984caea18fb0673bf319fcf28cef649.png"},{"username":"loslocitos","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15847901/medium/fe30d12fd2cf38212f929e13b169f9ec.jpeg"},{"username":"Oversleep","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15849065/medium/06141f13a6d541d753f3c2f2947b8068_default.png"},{"username":"Beardy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15865139/medium/fca6b9d2b3f52e286d1568f52b83b6a0_default.png"},{"username":"raelyan","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15875457/medium/2f4fda1d1aaa5dcc79b328baf3f03151.jpeg"},{"username":"dolphin738","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15913763/medium/ee6fede7b8528ca642329ada80d1cc18.png"},{"username":"brunotco","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15916719/medium/09db45880fc05abc18adb8d932a5ecf9_default.png"},{"username":"LukeIsHereToDevelop","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15924739/medium/3e6cd3940297bb4ba7bb039c93e6f918.png"},{"username":"kennit","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15925879/medium/6b0733ad3c5949b91c55e4d8b03db8a5_default.png"},{"username":"kuunpire","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15950309/medium/8192a4f08f07086828ac9f74ed29a169.jpeg"},{"username":"Sandor-dev","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15951759/medium/0216c2da4eb028164ebbecf1c72f6271_default.png"},{"username":"Meierschlumpf","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15953187/medium/23c744faef1ab84fbdc9351a7850aab6.jpeg"},{"username":"harmlesscat","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15970733/medium/dd8d0214a0250c932bb518b1b55e45a2_default.png"},{"username":"OrzWTF","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15976121/medium/4c4557cbff7ff7b0503455bc59c020e0.jpeg"},{"username":"tagaishi","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15977271/medium/eade504c83a5a1ff831c80a538fbdb44_default.png"},{"username":"azurite928","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15981895/medium/547ccc459ee123e78b5401c499f1022d.png"},{"username":"Ronner231","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16021342/medium/7734d550df2de5a2fec2ffff33e7024c.jpeg"},{"username":"ugyes","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16034148/medium/ed001e3f470a2dea9a8ce955b18e7bd5.png"},{"username":"rpieja","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16045554/medium/bd55baca2ef8b92502a760cc9ee7c505_default.png"},{"username":"flambyisyou","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16051620/medium/3a3bc0c90f6b95ab4ef74396a0a17beb.png"},{"username":"Topbcy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16077170/medium/d3aed33ea56330338756cfcd89477cfe.jpeg"},{"username":"ai5d02sb","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16084674/medium/7c8119fe2a5ca71bb15f636916a42b95_default.png"},{"username":"explosiveparrot","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16097722/medium/7762f80fc1da63f5b2eb87de9d640324_default.png"},{"username":"DestroyCom","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16111544/medium/83e3d3ac5c8bfed634cd1a074854db67.png"},{"username":"neotraxo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16116966/medium/b30df957873329019a8a90a69f6efe3a.png"},{"username":"gzxpa","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16123314/medium/487b05577310a946b0b98bc72c11213b_default.png"},{"username":"TORIK","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16124732/medium/de770890e6411860ac06e8cc2dc0abd0_default.png"},{"username":"sergio.pibot","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16138114/medium/80db802b2fcd6bbf19ea4c7413367dca.jpeg"},{"username":"karam-ajaj","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16139838/medium/7539ab958d3c85d5f67e7849483ea8f7.jpeg"},{"username":"SimonHenz97","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16151520/medium/9b94a85b536688e2d94538c384fd0a76.png"},{"username":"olsson82","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16198988/medium/eeaab4cba210c537424d4204754b604e.png"},{"username":"bo3bdo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16222806/medium/8652f3beacb6098dde5a4cd0a29fe80d.jpeg"},{"username":"AngryPlayer04","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16245192/medium/342715274f4dc31fd4132cdae9126188.jpeg"},{"username":"ehatamm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16245884/medium/979d5e5526fe545be9bf46db481f7182.jpeg"},{"username":"Balionelis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16246058/medium/986d62ba363594c517d966fe9178dcbb.png"},{"username":"kwargs","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16248816/medium/19ce11f04bf1ae2637e0483399c675f9_default.png"},{"username":"lucicu","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16273892/medium/3b09e8f3bead221ef98d6a45a16851dc_default.png"},{"username":"Michael5564445","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16357688/medium/9d9026a8bf3eaccb0417da240932ebf7.png"},{"username":"Netscream","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16359198/medium/694e0c8572dc8a47e6d63da31ec5b992.png"},{"username":"belicstefan04","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16368142/medium/4742dcfd7bf7ad7e33e37ac44fba7e0f.png"},{"username":"Hamad2066","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16371478/medium/f37bbc7685b6757defdbd1494236af61.jpeg"},{"username":"3ct0pl4sm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16399616/medium/ce796d1c40163975624c0a483e56374f.png"},{"username":"sanchez.marcell626","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16433865/medium/3ddde1314764cc2205ffdcd1ae719a91.png"},{"username":"Minoctis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16490079/medium/91fd0849303a4eca033d660d0a7b6854.jpeg"},{"username":"homarr","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16657003/medium/3afabfd3de949a54ff5d40ad04552aba.png"},{"username":"mwgg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16658379/medium/763dd09f5c238cc5324d4d5357985292.png"},{"username":"kiwinho","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16671855/medium/bfe3a2d497dc4748a04d02e4ae2139cb.jpeg"},{"username":"tjvg4m34r13","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16679135/medium/7a483b9b0abe0d925e54808e1cb38fa0.jpeg"},{"username":"Diegoem","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16705921/medium/29ce68ebc191fc3f65499511b12de98f_default.png"},{"username":"bouks69","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16746091/medium/b58b1fc08520de416198be9df5d093eb.jpeg"},{"username":"futhgar","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16748067/medium/b8c526b097f9aa9d687250cac3df16dc.jpeg"},{"username":"GrimSQL","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16764343/medium/fff729a76d1f8fbcdaee9bb0c0f5f3fd.png"},{"username":"chnikel","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16767047/medium/3dda2a37e03049dafefe6697bb00f75c.png"},{"username":"yuki19870616","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16767951/medium/f59c848e38382cda6c8464b0a7b9e642_default.png"},{"username":"noamdal","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16782245/medium/b4a72a61f9caf138f0322c0cb987e6d0.png"},{"username":"MalithRukshan","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16809057/medium/084d6a5b35fbbeacc70e7021197d222f.png"},{"username":"minirope19","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16930929/medium/e8cbbc26747e31c5a8959215e1dd067b.jpeg"},{"username":"prubin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17503492/medium/96ac87644691e49c1f983f2f094e541c_default.png"},{"username":"sam-tam-sam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17578052/medium/1628dc2954ce3ae95e0caaa2bd60ab0c.png"},{"username":"KydoxOwn","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17764383/medium/0bc150a09d77f5fda43988e6342777dc.png"},{"username":"toineenzo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12371852/medium/9f1b2bedb4794c59479cd289bdba9fab.png"},{"username":"ArcaneWater","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12421105/medium/1ac82517a6c658f6b62c48aebcb17e86_default.png"},{"username":"Ebedami","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12974801/medium/b596dea5c09454c4c828eac558389755.jpg"},{"username":"perrygamerptyt","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13117177/medium/1406c949507ecd438480473c8a1f6124.jpg"},{"username":"SergiSvK","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13320543/medium/3df3c79cc5f0c53716c559cd3fd2fc20.png"},{"username":"jeongeon","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13362856/medium/eb572f72cde5e5526c517ca3a6489850_default.png"},{"username":"MrTranCFCVN","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13439475/medium/a1789d69c85d90c667194b5d6a518c08_default.png"},{"username":"Tobost06","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13467449/medium/9227dc07f6366bc060d28304f5cb8ce7.jpeg"},{"username":"Kf637","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13530622/medium/118b00bb72d01d3229c97409637e25d1_default.png"},{"username":"Cruellest","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13610615/medium/2a74faa0947ee08d18f7946fb8602c1e.jpg"},{"username":"olavinto","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14424644/medium/37c01732bd2b7a272b421807a05ccfeb.png"},{"username":"Thario","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14491372/medium/dd5e142070802a672900121aa3f332a6_default.png"},{"username":"Wjouter","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14585620/medium/b91a51513332bdc0a0a9ecbed99ecbc3.png"},{"username":"albanobattistella","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14686194/medium/564afcfd76f5cd24751bd669cf4943aa.jpg"},{"username":"Kachelkaiser","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14763406/medium/203a781797da4a18fe0f4e7db8fd547c.png"},{"username":"regi4","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14842936/medium/f7e1d672f562a7db61ba6b4b8f4de41d.png"},{"username":"baton5129","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14891064/medium/8d438f557b82977dd02f0ead3187265f.PNG"},{"username":"meviper","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15958903/medium/209f521ce33a05831fa8d7d1b7babc20_default.png"},{"username":"GerOriki","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16268376/medium/da00e54b08cd40348231d92873a0cd0a_default.png"},{"username":"TheRedCyclops","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16486399/medium/7f39226180120d5a9efcb8e13d8aec8e_default.png"},{"username":"dymek37","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16659221/medium/d1dc6d11d7e7fc370efc5070e837ae03.png"},{"username":"B3CKDOOR","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16744975/medium/e6c7b2d45e76465d54e6efb67c83da7d.png"},{"username":"on9686","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16768173/medium/6282fed9aafe0262a173a5ec0887e5d9_default.png"},{"username":"Larsluph","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16779933/medium/ed7d36f8b44013bad3a16ac0491bd0f7.png"},{"username":"FOEDI","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16781001/medium/6b4552fb72e359aa016f7a054078fac8.png"},{"username":"xmarkurion","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16805443/medium/34a20428427de5aaef99c7d140646803.png"},{"username":"gregid","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16806705/medium/f3c8f4a20436e8538929130142bbc104.png"},{"username":"Flameberg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16820453/medium/d47be866148474c9d05fec6fbe406d88.png"},{"username":"Playfust","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16872865/medium/5f4609cc711e5fb9fa8a0b178635769c.jpeg"},{"username":"Cotignac","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16879297/medium/81ff353edf030699a159bb79cda26e84.jpg"},{"username":"LeaHun","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16916375/medium/618520f0543b1e212ebc83d66b683d4b_default.png"},{"username":"jackwellerreal","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16919717/medium/37bbfdc03893f141133ccc619593266d.png"},{"username":"renan.costaalencar","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16938427/medium/6d25b25320c5bde734118dec4af9cd5b.png"},{"username":"andrius.cim","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16967845/medium/46028fc1a0ddfaaeebcb37b39739625d_default.png"},{"username":"xjxexrxrxyx","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17041412/medium/d34d664166db6fb25579bca9ff3b50b4.jpeg"},{"username":"bilhal.fellah","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17044572/medium/d1ec6f17e4aa013e9eededc83778d2c6.png"},{"username":"the_octo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17058060/medium/7423ab3a4cabb67f5decb2e2dfa2b675_default.png"},{"username":"BamesJond","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17075846/medium/128d7ffd40254c8e376f45678e2fc620_default.png"},{"username":"Kornikus","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17134240/medium/d7a42b2c30db46ff152699ca01f9e11a_default.png"},{"username":"Master3307","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17161852/medium/94ec577dd53e16b434f2aac9c34f64c6.png"},{"username":"xoxyuxu","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17176526/medium/108055aa24343043de852e412bec74be.png"},{"username":"cryptodntsleep","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17249408/medium/b9e15aa2b24a0dbc1e4f194aa8ed5109.png"},{"username":"yaniorg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17261842/medium/7dbfc77f21c826845961243c44540d26.jpg"},{"username":"Vidariondr","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17262438/medium/45fbc71be02ef27296b6f423e97b6a03_default.png"},{"username":"Fastery","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17263066/medium/701b0edc59676174863ad651528bbb37.jpeg"},{"username":"stratself","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17290388/medium/57d9d8161b3fd0abf4dc5558b4a1fa05_default.png"},{"username":"wuzeqin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17292606/medium/b9e0308410b31337e2d7aa6ed346d718_default.png"},{"username":"ChristopherBull","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17304338/medium/21a175ce596bfae81430767467908b90.jpeg"},{"username":"darkbcnlord","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17311436/medium/b271bc0b419ec3a26ddb7b63ff8ffc4b.png"},{"username":"zhoran1997","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17344028/medium/c8f450b5a8f1e9494a45dc60af7ce63e.png"},{"username":"Pakura","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17396930/medium/18602738018348bf2ffa83a78bf165e2_default.png"},{"username":"gilalves1703","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17447768/medium/3f1de562a5c2391c88bbf8d5cf40a266.png"},{"username":"iServeCloud","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17470960/medium/11649b99d609438d66455a4d2fdc70f7.png"},{"username":"guibolzantvbox","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17474136/medium/59a576e6da538664ceb972253916fa02.jpeg"},{"username":"ivann136","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17506062/medium/67d09ada36fac5f444a1f7517e3a97bd_default.png"},{"username":"Maheshivara","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17516082/medium/576e1e6581cf969615b265de6aeff8b7.jpeg"},{"username":"brunogui81","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17517542/medium/7c0d174e810435af518fe8e0c05e151f.jpeg"},{"username":"michelwilhelm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17542190/medium/4c6b33fd8993ff565fa75add2ce8355e.jpeg"},{"username":"dias-gxstavo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17547376/medium/8c6b109aae8aeb651192346a39ae9e5b.jpeg"},{"username":"ToxykAuBleu","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17614030/medium/8d21e0608560172b121bd46126d827c7.jpeg"},{"username":"e.erdemir","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17617994/medium/7259a29db9941c6171a8cbbd643b92bd.png"},{"username":"sjonkreutz","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17618306/medium/7601c45d015189a863656e21348d223b.png"},{"username":"volovich0377","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17620388/medium/c929a8fbd24f96d17620fa497ade7309.png"},{"username":"Hamstio1","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17624898/medium/406e0bf40094c24a01c3064f8f5b2a82.png"},{"username":"i3laa","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17627462/medium/613a9d283ed8b69819ee181903851660.jpeg"},{"username":"bastiengrignon","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17629884/medium/ddfc3339285bd65195fcd834e512e9b7.jpeg"},{"username":"kauhee","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17644600/medium/def1b9066cf9275b4273a5a5dca7f325.png"},{"username":"hongminzheng","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17683860/medium/a764f4963912777d27a7cd98e136a2b2.png"},{"username":"d-lindahl","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17726729/medium/cbde348582fbabcfadd89a56cd5a0b40.png"},{"username":"Enoal-Fauchille-Bolle","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17734649/medium/6e6fb29198782dc7856ea21811db669c.jpeg"},{"username":"manos-grigorakis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17737973/medium/b37f685587c920c81fdaf9a357962434.jpeg"},{"username":"SertraFurr","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17738703/medium/5f1e9f270e315ee3185cf2ccb3f5a833.png"},{"username":"Kle.Ma","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17739207/medium/b767d6093e6608f251ca152f53d359b8_default.png"},{"username":"WalkerLukas","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17744485/medium/87508ce83a9b6e5391f55883a66a054b.png"},{"username":"mtya","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17748447/medium/f574d5a770d74814f8c49ee68e00f5b7.jpeg"},{"username":"Elaktrato","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17750977/medium/f4109d379f29dc8ea2e7799ac9e208c8.png"},{"username":"marcelmeis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17753627/medium/4ba32116c27e427d3073e45713f6564e.png"},{"username":"ProKafelek12e","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17760377/medium/5199af152f1ea42cee718e7729bd41ff.png"},{"username":"zalaghi","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17763367/medium/a06f7b15222663517b58b8dc8902f8a1.png"}] \ No newline at end of file +[{"username":"carlchina","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/6697/medium/d22bbe7797bbeb30dbdc73a5648d329a_default.png"},{"username":"zielmann","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/7795/medium/ad22b8b8d5eb33e4154d53a454c862fd_default.png"},{"username":"Thalyn","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12494028/medium/5faf5acc945a874ed116c4567edaa09f_default.png"},{"username":"magnushj","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12503488/medium/68f32e33ea7b891960055c9f831f7344_default.png"},{"username":"almontegil","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12580457/medium/f4136cacbdfdb4c28ae7f85dc5f840db_default.png"},{"username":"andibing","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12664938/medium/b8be63e4dcb2e791ced1ffc9e3a049a5.jpg"},{"username":"gronare","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12680911/medium/c6eb14d1504156f34a158e507f73aa3d_default.png"},{"username":"SmartPhoneLover","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12701640/medium/ec95486662ec875cda080e778c3ff702.jpg"},{"username":"marcelotk_15","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12773227/medium/ecf6bea16ab0c873c131df2154ee9718_default.png"},{"username":"S3OD177","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13051544/medium/11dd990837f61f7e8f7d36b041a7fddb_default.png"},{"username":"marcg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13153301/medium/ebcf60fbf27deb148ec40a659566c6ff_default.png"},{"username":"BeersTeddy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13185230/medium/2f1f4e1effe74a23422b195cbefb2a95_default.png"},{"username":"jamesmcmahon0","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13245578/medium/586aa873b4abddbd9abc6f3de99ab70e.jpeg"},{"username":"b1nar10","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13286752/medium/aaa1f083e04c572f953fde4563171e4a.jpeg"},{"username":"vannCN","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13330448/medium/9b8a9ee3611e51e951e22d5fd4eb7d8d.jpg"},{"username":"binswm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13343482/medium/45dde0e6097b9b72705d2eba9dbbc276_default.png"},{"username":"SkewRam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13496556/medium/188f5c2deb7938eda51eb786cc4539ca.jpeg"},{"username":"raphcatarino","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13547726/medium/b003511e67df13a4b4b5689488fa8099.jpg"},{"username":"shillos5","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13600557/medium/b17650b2b84f829d54fa548c44c1ab27.jpeg"},{"username":"wolong98","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13641407/medium/f4634edc58c7857a357e5293543c15cf.jpg"},{"username":"spair0039","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14012333/medium/72430e96027c09c19141cac38eae4617.png"},{"username":"MajorMarcin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14072183/medium/0a197e64bb92b6ea94cecbb607512c45.jpg"},{"username":"KosmoMoustache","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14103535/medium/51c438cdd9f86e8811c75f4869de479c.png"},{"username":"Soochaehwa","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14118689/medium/496a1ce63111547bf455a1e0a7ac75f1_default.png"},{"username":"garryfield","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14187035/medium/520c964706676045f6eeec1b08edc880.jpg"},{"username":"droidenko","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14444264/medium/4aa3a8e824f72bc9e5ec0d8de307095e.jpeg"},{"username":"maathis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14489950/medium/6eeddf13a757ee35f78f1763f94d95e1.jpeg"},{"username":"bukvam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14546502/medium/a819dc40a13c57395a17dc46e847d3ce.jpg"},{"username":"gm.cinalli","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14670666/medium/2d466a0fdbda40764526be86c97c0ab4.jpeg"},{"username":"antoine2tt","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14722148/medium/f88d926900862dd59007ea4b3419cb9d.png"},{"username":"cretzen","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14799754/medium/3ba5c1805ec20c000d5810c07985afe9.jpg"},{"username":"ktKongTong","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14817246/medium/97cfc3c028dbdaf85ebd1102da71e58c.jpeg"},{"username":"marinkaberg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14861042/medium/2d5c4e62613f03082f3e645fa92efd59.jpeg"},{"username":"djismgaming","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14934947/medium/f5a8570713c34ab0f7d5405d105e2a9a.jpeg"},{"username":"f1refa11","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14949159/medium/fd2ae63b8eb4462200ba96abf943c1b9.png"},{"username":"jeffersonraimon","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15057621/medium/6dff469563860e2618bc9d45646d0ab4.png"},{"username":"giop98","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15057987/medium/b8a4825d3fc39fc662f35ee258db4b2d.jpeg"},{"username":"tomislav.kraljevic","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15118857/medium/e133f1061cc92850b854d05d8faaeafd.png"},{"username":"Walkx","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15202182/medium/5c37361ae45aeed487b34582c1f7ca37.png"},{"username":"andrea.rosso","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15246318/medium/11f3f5ef44ec7f55b6f143090e208704_default.png"},{"username":"ajnart","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15270132/medium/a0f107a463c8910ee96bc2fa843a17e3.jpeg"},{"username":"Witchling","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15282238/medium/9e196b6783415b10ca571b7673c2ef33.jpg"},{"username":"guineuu","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15292058/medium/22fbb758bda3b7805d50bf21d38f2c20.jpeg"},{"username":"Bulgus","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15304568/medium/0e8787e5ceb02ed5c96a514d0068ae87.jpg"},{"username":"Narno","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15314996/medium/a6ba093f24884250a1bf6c6f2ca50f1b_default.png"},{"username":"BySempron","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15315986/medium/e3d22d7b1423c6823a9f36d595ed4bdb.png"},{"username":"jonathan.berglin.work","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15359236/medium/95930b2093db13b76179782f7322c5d5.png"},{"username":"icdmkg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15405504/medium/f334d16cde1ac4ffb935b440d03465af.gif"},{"username":"irithys","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15405614/medium/3086461c47cce0a0c031925e5f943412.png"},{"username":"JannesV","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15419912/medium/a7809eb4b817d7c49b62cf10ae86b950.png"},{"username":"benniblot","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15419914/medium/8086b1573d31f152ba41949b3a172445.jpeg"},{"username":"pacjo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15419916/medium/3cbeddbd7bc01faafb5a3bf47bba915b_default.png"},{"username":"Payou6994","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15419934/medium/37c9b9b17dfb578404c1c1ddb73ba7a8.png"},{"username":"fzibi21","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15419976/medium/0da688450358e0290a7b7359cc1f7328.png"},{"username":"WowMurdock","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15420118/medium/de19576fccb495b6dfe4c4c04a56b834.png"},{"username":"hbooo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15420120/medium/6c7c7f8db785061356ebb03d044d3329.jpeg"},{"username":"Manicraft1001","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15420178/medium/50ec94563a06a9f74f33bd09f01eed4d.jpg"},{"username":"Void123","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15420354/medium/86929d44df92a00f9fe900a985c196df_default.png"},{"username":"R4cc","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15422606/medium/a390979662b84694f59de30bdb732141.jpeg"},{"username":"fabricionaweb","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15425808/medium/a9354142e7ae5152c144409d55fda551_default.png"},{"username":"JokeOfDead","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15426890/medium/a383eac9365e9de64fd4ab1d6fd0cb95.jpeg"},{"username":"hkz","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15427174/medium/c88acefb0d7306e1f7470e872029fb39_default.png"},{"username":"Steken","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15428516/medium/d5aea5653c769c3a523182bdb60d1664.png"},{"username":"flar.anton","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15428592/medium/b01fdb365d892e9f811f77fcb50a80a2.jpeg"},{"username":"Bon","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15433542/medium/5397da4dfc821f20b6ac14fe0c514e9a.jpeg"},{"username":"bfkadan","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15434162/medium/4f9a0b43cfe3acaea60124c14ba7f44a.png"},{"username":"BunnySweety","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15434662/medium/f0ef200a6a0dcf0e1d0e9ecd4148f560_default.png"},{"username":"HRKings","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15436168/medium/69a31e461d38549f01864e7ef10d642e.png"},{"username":"wiston81","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15439078/medium/10f292c8d12a7c21a44b54495fa4a3d8.jpeg"},{"username":"qaz0911","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15440860/medium/3d3e50ee388c72dc4bf7a771761f2d89_default.png"},{"username":"qqyule","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15441462/medium/7a3cdf82710ffb5d8f388bc0bd010665.png"},{"username":"Bims0n","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15445560/medium/603220b603eeb3367e0f0d3fa675247c.jpg"},{"username":"TariqDaCoder","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15446228/medium/50b0f4040112bbd67690b769477398e5_default.png"},{"username":"Anarchon","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15449644/medium/e925e1f3e3ffbf0f982391ce263a1a28.jpeg"},{"username":"Ashun","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15453020/medium/ccdcf51c73d6aae40751bb30beee1915_default.png"},{"username":"sebekmartin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15454038/medium/bcfb44598cdfd1d7cd4eb35812538962.jpeg"},{"username":"RagnarGraves","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15459882/medium/143d5af850c1154070a218bea124e9cb_default.png"},{"username":"PrtmPhlp","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15462414/medium/b80db55e9de301432dcd1f8c8b24fd49_default.png"},{"username":"DooYoo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15470768/medium/2a18cf4be67094724b508c9e1e698a21_default.png"},{"username":"espentruls","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15486092/medium/8e38afc3a4ff669226a0cfd3e420ff3a.jpeg"},{"username":"frisco82","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15486922/medium/4e1c5d4189b42508e660daa3c1c25b2a.jpeg"},{"username":"lupineDK","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15491798/medium/461bd501e8371c062bf29ea171aedd36_default.png"},{"username":"hillaliy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15492732/medium/0bae17b421604892d888e3fc70cf0587.jpeg"},{"username":"MarcOrfilaCarreras","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15501072/medium/ea52b26c3c6f21e4931e38e3ce3f3d6e.png"},{"username":"robertbridda","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15507822/medium/a368c2e30411bb2da9b49290084191f3.png"},{"username":"MKoniuszko","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15518090/medium/87605434fcc839f6763ab07c50f6d232.jpeg"},{"username":"HeroSizy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15518710/medium/4e79c0e98cbeb536dd961e656331b509.png"},{"username":"dwt136","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15520022/medium/554422503f2baea43ace85facb4546fb_default.png"},{"username":"asifthewebguy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15526719/medium/b18931dd0c800d725048bd440646198b_default.png"},{"username":"eiloogs","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15545537/medium/f290a2f1190983530a9b76b2e858a609.gif"},{"username":"_vytdv","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15547289/medium/60d8644cc8ad6f11a92ccea4a14cd098_default.png"},{"username":"crendasien","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15554645/medium/598ab1d4aaf6b8dccd5ba16be92da7b9.jpeg"},{"username":"edxo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15573823/medium/8a565b732a75a77f840dd123cdb30bf4.png"},{"username":"somerlev","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15617065/medium/f4b13513e311ec902d90b2f718412c55.jpg"},{"username":"kid1412621","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15643771/medium/dd455e32de652fa88e6fd97598bdffa7.png"},{"username":"suming","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15644717/medium/244159dfe10fa03436205506f80c9e25.png"},{"username":"nick.gher","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15647517/medium/5374a2c6fef60a3fbf0edf86b997c351_default.png"},{"username":"DataCat","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15650315/medium/ce7c1365adf35c5d490d77500a4607fb_default.png"},{"username":"dizo89","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15658375/medium/0ca745e5017d491fe1b22b0239904de8.jpeg"},{"username":"tee_noodle","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15674577/medium/804d9cf06f5196026acb6436b809d0da_default.png"},{"username":"Marty88","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15674593/medium/492b1509d52bd2809dea768121217125.jpeg"},{"username":"Spillebulle","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15677023/medium/20b98bb85062e07afe0e63618f8a5feb.PNG"},{"username":"petitmewen","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15685239/medium/15de9b62d2e0bc25013435f1784bbcc1.jpeg"},{"username":"NoProsNoNoobs","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15687709/medium/ae8f65fafeb8bcf74dcb8871bbe46461.png"},{"username":"y.gybson","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15690777/medium/b5cb5d2d5768904ff6586c727e3a6c77.jpeg"},{"username":"inside90","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15704947/medium/c1355fcb30dd76f8e39d98d1d49f1c52.png"},{"username":"RJSkudra","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15709853/medium/c3abf2774913dc4e81fb261d36d7668c.png"},{"username":"binge203","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15713937/medium/77c1cfa4314673db80e6881fd9f64668.gif"},{"username":"Mailootje","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15714337/medium/743c3bc4ab1989966a375eeeec83d8b8.jpeg"},{"username":"tim-wiegers","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15719805/medium/08a96f064813350661cd6b20bf3d7d99.png"},{"username":"GkhnG","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15722911/medium/71a027caec489ef6ce82bcf1888329d0_default.png"},{"username":"Zoen-Millo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15760967/medium/9e956f11adc5b34f5636268b5c485dbf.jpg"},{"username":"HooinKyoma","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15792897/medium/9489f0a9b368e0e827ae758b740a2eed.jpeg"},{"username":"wathergeen","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15794495/medium/a83b7fc80d180669ebfd6deb3544cfd0.png"},{"username":"MoeToo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15818233/medium/d35cd6953717706eaf20f6c143c62947.png"},{"username":"itodouble","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15836233/medium/e984caea18fb0673bf319fcf28cef649.png"},{"username":"loslocitos","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15847901/medium/fe30d12fd2cf38212f929e13b169f9ec.jpeg"},{"username":"Oversleep","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15849065/medium/06141f13a6d541d753f3c2f2947b8068_default.png"},{"username":"Beardy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15865139/medium/fca6b9d2b3f52e286d1568f52b83b6a0_default.png"},{"username":"raelyan","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15875457/medium/2f4fda1d1aaa5dcc79b328baf3f03151.jpeg"},{"username":"dolphin738","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15913763/medium/ee6fede7b8528ca642329ada80d1cc18.png"},{"username":"brunotco","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15916719/medium/09db45880fc05abc18adb8d932a5ecf9_default.png"},{"username":"LukeIsHereToDevelop","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15924739/medium/3e6cd3940297bb4ba7bb039c93e6f918.png"},{"username":"kennit","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15925879/medium/6b0733ad3c5949b91c55e4d8b03db8a5_default.png"},{"username":"kuunpire","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15950309/medium/8192a4f08f07086828ac9f74ed29a169.jpeg"},{"username":"Sandor-dev","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15951759/medium/0216c2da4eb028164ebbecf1c72f6271_default.png"},{"username":"Meierschlumpf","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15953187/medium/23c744faef1ab84fbdc9351a7850aab6.jpeg"},{"username":"harmlesscat","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15970733/medium/dd8d0214a0250c932bb518b1b55e45a2_default.png"},{"username":"OrzWTF","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15976121/medium/4c4557cbff7ff7b0503455bc59c020e0.jpeg"},{"username":"tagaishi","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15977271/medium/eade504c83a5a1ff831c80a538fbdb44_default.png"},{"username":"azurite928","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15981895/medium/547ccc459ee123e78b5401c499f1022d.png"},{"username":"Ronner231","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16021342/medium/7734d550df2de5a2fec2ffff33e7024c.jpeg"},{"username":"ugyes","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16034148/medium/ed001e3f470a2dea9a8ce955b18e7bd5.png"},{"username":"rpieja","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16045554/medium/bd55baca2ef8b92502a760cc9ee7c505_default.png"},{"username":"flambyisyou","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16051620/medium/3a3bc0c90f6b95ab4ef74396a0a17beb.png"},{"username":"Topbcy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16077170/medium/d3aed33ea56330338756cfcd89477cfe.jpeg"},{"username":"ai5d02sb","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16084674/medium/7c8119fe2a5ca71bb15f636916a42b95_default.png"},{"username":"explosiveparrot","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16097722/medium/7762f80fc1da63f5b2eb87de9d640324_default.png"},{"username":"DestroyCom","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16111544/medium/83e3d3ac5c8bfed634cd1a074854db67.png"},{"username":"neotraxo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16116966/medium/b30df957873329019a8a90a69f6efe3a.png"},{"username":"gzxpa","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16123314/medium/487b05577310a946b0b98bc72c11213b_default.png"},{"username":"TORIK","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16124732/medium/de770890e6411860ac06e8cc2dc0abd0_default.png"},{"username":"sergio.pibot","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16138114/medium/80db802b2fcd6bbf19ea4c7413367dca.jpeg"},{"username":"karam-ajaj","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16139838/medium/7539ab958d3c85d5f67e7849483ea8f7.jpeg"},{"username":"SimonHenz97","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16151520/medium/9b94a85b536688e2d94538c384fd0a76.png"},{"username":"olsson82","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16198988/medium/eeaab4cba210c537424d4204754b604e.png"},{"username":"bo3bdo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16222806/medium/8652f3beacb6098dde5a4cd0a29fe80d.jpeg"},{"username":"AngryPlayer04","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16245192/medium/342715274f4dc31fd4132cdae9126188.jpeg"},{"username":"ehatamm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16245884/medium/979d5e5526fe545be9bf46db481f7182.jpeg"},{"username":"Balionelis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16246058/medium/986d62ba363594c517d966fe9178dcbb.png"},{"username":"kwargs","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16248816/medium/19ce11f04bf1ae2637e0483399c675f9_default.png"},{"username":"lucicu","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16273892/medium/3b09e8f3bead221ef98d6a45a16851dc_default.png"},{"username":"Michael5564445","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16357688/medium/9d9026a8bf3eaccb0417da240932ebf7.png"},{"username":"Netscream","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16359198/medium/694e0c8572dc8a47e6d63da31ec5b992.png"},{"username":"belicstefan04","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16368142/medium/4742dcfd7bf7ad7e33e37ac44fba7e0f.png"},{"username":"Hamad2066","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16371478/medium/f37bbc7685b6757defdbd1494236af61.jpeg"},{"username":"3ct0pl4sm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16399616/medium/ce796d1c40163975624c0a483e56374f.png"},{"username":"sanchez.marcell626","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16433865/medium/3ddde1314764cc2205ffdcd1ae719a91.png"},{"username":"Minoctis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16490079/medium/91fd0849303a4eca033d660d0a7b6854.jpeg"},{"username":"homarr","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16657003/medium/3afabfd3de949a54ff5d40ad04552aba.png"},{"username":"mwgg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16658379/medium/763dd09f5c238cc5324d4d5357985292.png"},{"username":"kiwinho","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16671855/medium/bfe3a2d497dc4748a04d02e4ae2139cb.jpeg"},{"username":"tjvg4m34r13","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16679135/medium/7a483b9b0abe0d925e54808e1cb38fa0.jpeg"},{"username":"Diegoem","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16705921/medium/29ce68ebc191fc3f65499511b12de98f_default.png"},{"username":"bouks69","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16746091/medium/b58b1fc08520de416198be9df5d093eb.jpeg"},{"username":"futhgar","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16748067/medium/b8c526b097f9aa9d687250cac3df16dc.jpeg"},{"username":"GrimSQL","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16764343/medium/fff729a76d1f8fbcdaee9bb0c0f5f3fd.png"},{"username":"chnikel","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16767047/medium/3dda2a37e03049dafefe6697bb00f75c.png"},{"username":"yuki19870616","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16767951/medium/f59c848e38382cda6c8464b0a7b9e642_default.png"},{"username":"noamdal","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16782245/medium/b4a72a61f9caf138f0322c0cb987e6d0.png"},{"username":"MalithRukshan","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16809057/medium/084d6a5b35fbbeacc70e7021197d222f.png"},{"username":"minirope19","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16930929/medium/e8cbbc26747e31c5a8959215e1dd067b.jpeg"},{"username":"prubin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17503492/medium/96ac87644691e49c1f983f2f094e541c_default.png"},{"username":"sam-tam-sam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17578052/medium/1628dc2954ce3ae95e0caaa2bd60ab0c.png"},{"username":"KydoxOwn","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17764383/medium/0bc150a09d77f5fda43988e6342777dc.png"},{"username":"toineenzo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12371852/medium/9f1b2bedb4794c59479cd289bdba9fab.png"},{"username":"ArcaneWater","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12421105/medium/1ac82517a6c658f6b62c48aebcb17e86_default.png"},{"username":"Ebedami","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12974801/medium/b596dea5c09454c4c828eac558389755.jpg"},{"username":"perrygamerptyt","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13117177/medium/1406c949507ecd438480473c8a1f6124.jpg"},{"username":"SergiSvK","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13320543/medium/3df3c79cc5f0c53716c559cd3fd2fc20.png"},{"username":"jeongeon","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13362856/medium/eb572f72cde5e5526c517ca3a6489850_default.png"},{"username":"MrTranCFCVN","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13439475/medium/a1789d69c85d90c667194b5d6a518c08_default.png"},{"username":"Tobost06","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13467449/medium/9227dc07f6366bc060d28304f5cb8ce7.jpeg"},{"username":"Kf637","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13530622/medium/118b00bb72d01d3229c97409637e25d1_default.png"},{"username":"Cruellest","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13610615/medium/2a74faa0947ee08d18f7946fb8602c1e.jpg"},{"username":"olavinto","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14424644/medium/37c01732bd2b7a272b421807a05ccfeb.png"},{"username":"OzakIOne","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14446246/medium/3abb08846344706d3a16a9fb196655ee.jpeg"},{"username":"Thario","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14491372/medium/dd5e142070802a672900121aa3f332a6_default.png"},{"username":"Wjouter","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14585620/medium/b91a51513332bdc0a0a9ecbed99ecbc3.png"},{"username":"albanobattistella","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14686194/medium/564afcfd76f5cd24751bd669cf4943aa.jpg"},{"username":"angrystar170","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14736454/medium/7a70edc6b50122877533e95a4e971203.jpeg"},{"username":"Kachelkaiser","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14763406/medium/203a781797da4a18fe0f4e7db8fd547c.png"},{"username":"regi4","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14842936/medium/f7e1d672f562a7db61ba6b4b8f4de41d.png"},{"username":"baton5129","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14891064/medium/8d438f557b82977dd02f0ead3187265f.PNG"},{"username":"legocuedy09","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15206280/medium/a93129b77a54ffbb2c9f136f18718e80.png"},{"username":"meviper","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15958903/medium/209f521ce33a05831fa8d7d1b7babc20_default.png"},{"username":"GerOriki","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16268376/medium/da00e54b08cd40348231d92873a0cd0a_default.png"},{"username":"TheRedCyclops","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16486399/medium/7f39226180120d5a9efcb8e13d8aec8e_default.png"},{"username":"dymek37","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16659221/medium/d1dc6d11d7e7fc370efc5070e837ae03.png"},{"username":"B3CKDOOR","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16744975/medium/e6c7b2d45e76465d54e6efb67c83da7d.png"},{"username":"on9686","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16768173/medium/6282fed9aafe0262a173a5ec0887e5d9_default.png"},{"username":"Larsluph","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16779933/medium/ed7d36f8b44013bad3a16ac0491bd0f7.png"},{"username":"FOEDI","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16781001/medium/6b4552fb72e359aa016f7a054078fac8.png"},{"username":"xmarkurion","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16805443/medium/34a20428427de5aaef99c7d140646803.png"},{"username":"gregid","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16806705/medium/f3c8f4a20436e8538929130142bbc104.png"},{"username":"Flameberg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16820453/medium/d47be866148474c9d05fec6fbe406d88.png"},{"username":"Playfust","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16872865/medium/5f4609cc711e5fb9fa8a0b178635769c.jpeg"},{"username":"Cotignac","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16879297/medium/81ff353edf030699a159bb79cda26e84.jpg"},{"username":"LeaHun","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16916375/medium/618520f0543b1e212ebc83d66b683d4b_default.png"},{"username":"jackwellerreal","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16919717/medium/37bbfdc03893f141133ccc619593266d.png"},{"username":"renan.costaalencar","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16938427/medium/6d25b25320c5bde734118dec4af9cd5b.png"},{"username":"andrius.cim","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16967845/medium/46028fc1a0ddfaaeebcb37b39739625d_default.png"},{"username":"xjxexrxrxyx","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17041412/medium/d34d664166db6fb25579bca9ff3b50b4.jpeg"},{"username":"bilhal.fellah","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17044572/medium/d1ec6f17e4aa013e9eededc83778d2c6.png"},{"username":"the_octo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17058060/medium/7423ab3a4cabb67f5decb2e2dfa2b675_default.png"},{"username":"BamesJond","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17075846/medium/128d7ffd40254c8e376f45678e2fc620_default.png"},{"username":"Kornikus","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17134240/medium/d7a42b2c30db46ff152699ca01f9e11a_default.png"},{"username":"Master3307","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17161852/medium/94ec577dd53e16b434f2aac9c34f64c6.png"},{"username":"xoxyuxu","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17176526/medium/108055aa24343043de852e412bec74be.png"},{"username":"cryptodntsleep","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17249408/medium/b9e15aa2b24a0dbc1e4f194aa8ed5109.png"},{"username":"yaniorg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17261842/medium/7dbfc77f21c826845961243c44540d26.jpg"},{"username":"Vidariondr","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17262438/medium/45fbc71be02ef27296b6f423e97b6a03_default.png"},{"username":"Fastery","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17263066/medium/701b0edc59676174863ad651528bbb37.jpeg"},{"username":"stratself","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17290388/medium/57d9d8161b3fd0abf4dc5558b4a1fa05_default.png"},{"username":"wuzeqin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17292606/medium/b9e0308410b31337e2d7aa6ed346d718_default.png"},{"username":"ChristopherBull","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17304338/medium/21a175ce596bfae81430767467908b90.jpeg"},{"username":"darkbcnlord","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17311436/medium/b271bc0b419ec3a26ddb7b63ff8ffc4b.png"},{"username":"zhoran1997","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17344028/medium/c8f450b5a8f1e9494a45dc60af7ce63e.png"},{"username":"Pakura","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17396930/medium/18602738018348bf2ffa83a78bf165e2_default.png"},{"username":"gilalves1703","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17447768/medium/3f1de562a5c2391c88bbf8d5cf40a266.png"},{"username":"iServeCloud","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17470960/medium/11649b99d609438d66455a4d2fdc70f7.png"},{"username":"guibolzantvbox","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17474136/medium/59a576e6da538664ceb972253916fa02.jpeg"},{"username":"ivann136","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17506062/medium/67d09ada36fac5f444a1f7517e3a97bd_default.png"},{"username":"Maheshivara","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17516082/medium/576e1e6581cf969615b265de6aeff8b7.jpeg"},{"username":"brunogui81","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17517542/medium/7c0d174e810435af518fe8e0c05e151f.jpeg"},{"username":"michelwilhelm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17542190/medium/4c6b33fd8993ff565fa75add2ce8355e.jpeg"},{"username":"dias-gxstavo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17547376/medium/8c6b109aae8aeb651192346a39ae9e5b.jpeg"},{"username":"ToxykAuBleu","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17614030/medium/8d21e0608560172b121bd46126d827c7.jpeg"},{"username":"e.erdemir","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17617994/medium/7259a29db9941c6171a8cbbd643b92bd.png"},{"username":"sjonkreutz","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17618306/medium/7601c45d015189a863656e21348d223b.png"},{"username":"volovich0377","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17620388/medium/c929a8fbd24f96d17620fa497ade7309.png"},{"username":"Hamstio1","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17624898/medium/406e0bf40094c24a01c3064f8f5b2a82.png"},{"username":"i3laa","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17627462/medium/613a9d283ed8b69819ee181903851660.jpeg"},{"username":"bastiengrignon","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17629884/medium/ddfc3339285bd65195fcd834e512e9b7.jpeg"},{"username":"kauhee","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17644600/medium/def1b9066cf9275b4273a5a5dca7f325.png"},{"username":"hongminzheng","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17683860/medium/a764f4963912777d27a7cd98e136a2b2.png"},{"username":"d-lindahl","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17726729/medium/cbde348582fbabcfadd89a56cd5a0b40.png"},{"username":"Enoal-Fauchille-Bolle","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17734649/medium/6e6fb29198782dc7856ea21811db669c.jpeg"},{"username":"manos-grigorakis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17737973/medium/b37f685587c920c81fdaf9a357962434.jpeg"},{"username":"SertraFurr","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17738703/medium/5f1e9f270e315ee3185cf2ccb3f5a833.png"},{"username":"Kle.Ma","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17739207/medium/b767d6093e6608f251ca152f53d359b8_default.png"},{"username":"WalkerLukas","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17744485/medium/87508ce83a9b6e5391f55883a66a054b.png"},{"username":"mtya","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17748447/medium/f574d5a770d74814f8c49ee68e00f5b7.jpeg"},{"username":"Elaktrato","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17750977/medium/f4109d379f29dc8ea2e7799ac9e208c8.png"},{"username":"marcelmeis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17753627/medium/4ba32116c27e427d3073e45713f6564e.png"},{"username":"ProKafelek12e","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17760377/medium/5199af152f1ea42cee718e7729bd41ff.png"},{"username":"zalaghi","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17763367/medium/a06f7b15222663517b58b8dc8902f8a1.png"},{"username":"Amato21","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17776775/medium/214b1f1581a85fe10f1ae3241aab43e4.png"},{"username":"jabalv","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17778347/medium/14dd47fff64f6543e6015be7373420c3.png"}] \ No newline at end of file diff --git a/tooling/github/setup/action.yml b/tooling/github/setup/action.yml index 77ff8d742..4f8330014 100644 --- a/tooling/github/setup/action.yml +++ b/tooling/github/setup/action.yml @@ -7,7 +7,7 @@ runs: - uses: pnpm/action-setup@v6 - uses: actions/setup-node@v6 with: - node-version: 24.17.0 + node-version: 24.18.0 cache: "pnpm" - shell: bash