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 b7ec9461f..c15f08874 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -33,6 +33,7 @@ body: options: # The below comment is used to insert a new version with on-release.yml #NEXT_VERSION# + - 1.69.2 - 1.69.0 - 1.68.0 - 1.67.0 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 98a247460..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 @@ -163,65 +163,3 @@ jobs: run: cp .env.example .env - name: Build run: pnpm turbo build --affected - - memory-regression: - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@v6 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 - - name: Build Docker image - uses: docker/build-push-action@v7 - with: - platforms: linux/amd64 - context: . - push: false - load: true - tags: homarr-memory-test - network: host - cache-from: type=gha - cache-to: type=gha,mode=max - env: - SKIP_ENV_VALIDATION: true - - name: Start container and measure memory - run: | - docker run -d --name homarr-mem-test -p 17575:7575 homarr-memory-test - # Wait for container to be healthy (up to 60s) - healthy=false - for i in $(seq 1 30); do - if curl -sf http://localhost:17575/api/health 2>/dev/null; then - echo "Container healthy after $((i * 2))s" - healthy=true - break - fi - sleep 2 - done - if [ "$healthy" != "true" ]; then - echo "ERROR: Container never became healthy" - docker logs homarr-mem-test --tail 50 - exit 1 - fi - # Warm up: hit the dashboard page a few times - for i in $(seq 1 5); do - curl -sf http://localhost:17575/ > /dev/null 2>&1 || true - sleep 2 - done - # Measure RSS after warmup - MEM_KB=$(docker stats --no-stream --format '{{.MemUsage}}' homarr-mem-test | sed 's| /.*||') - echo "Raw memory: $MEM_KB" - # Convert to numeric MB (handles GiB and MiB suffixes) - MEM_NUM=$(echo "$MEM_KB" | awk '{ - if ($0 ~ /GiB/) { gsub(/GiB/,""); print $1 * 1024 } - else if ($0 ~ /MiB/) { gsub(/MiB/,""); print $1 } - else if ($0 ~ /KiB/) { gsub(/KiB/,""); print $1 / 1024 } - else { print $1 / 1048576 } - }') - echo "Memory usage: ${MEM_NUM} MB" - MAX_MB=700 - if awk "BEGIN {exit !($MEM_NUM > $MAX_MB)}"; then - echo "ERROR: Memory usage ${MEM_NUM} MB exceeds ${MAX_MB} MB threshold" - docker logs homarr-mem-test --tail 50 - exit 1 - fi - echo "Memory check passed: ${MEM_NUM} MB <= ${MAX_MB} MB" 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 d80cb5e1f..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 @@ -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/CODEOWNERS b/CODEOWNERS index ad494df91..7082b9bac 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -19,6 +19,14 @@ packages/integrations/src/anchor @ohmzi 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 @@ -127,6 +135,13 @@ packages/integrations/src/overseerr @manuel-rw 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 @@ -159,6 +174,10 @@ packages/integrations/src/search-ch @Meierschlumpf 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 @@ -187,6 +206,14 @@ packages/integrations/src/unraid @manuel-rw 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 diff --git a/apps/docs/docs/getting-started/installation/helm.md b/apps/docs/docs/getting-started/installation/helm.md index d6a4b7342..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.20.0](https://img.shields.io/badge/Version-8.20.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.68.0](https://img.shields.io/badge/AppVersion-v1.68.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 @@ -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/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/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/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/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/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/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/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/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 3fd82711d..586fa53b6 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -32,7 +32,7 @@ "@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", @@ -43,8 +43,8 @@ "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 636df3f52..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,7 +15,7 @@ const withNextIntl = createNextIntlPlugin({ const nextConfig: NextConfig = { env: { - HOMARR_VERSION: homarrVersion, + HOMARR_VERSION: process.env.HOMARR_VERSION ?? "unknown", }, output: "standalone", reactStrictMode: true, @@ -43,9 +31,19 @@ const nextConfig: NextConfig = { experimental: { optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"], turbopackFileSystemCacheForDev: true, - preloadEntriesOnStart: false, 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 36bb405ad..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 .", diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx index ee45a8b35..d67002432 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx @@ -79,14 +79,16 @@ export const createBoardContentPage = >( const userId = session?.user.id ?? "anonymous"; return ( - - - - - - - - + <> + + + + + + + + + ); }, generateMetadataAsync: async ({ params }: { params: Promise }): Promise => { @@ -123,9 +125,9 @@ async function QueryCacheHydration({ userId, boardId }: { userId: string; boardI const serialized = await getQueryCacheAsync(userId, boardId); if (!serialized) return null; - const persisted = superjson.parse(serialized); - if (persisted.buster !== queryCacheBuster) return null; - if (!persisted?.clientState?.queries?.length) return null; + const persisted = superjson.parse(serialized); + if (!persisted || persisted.buster !== queryCacheBuster) return null; + if (!persisted.clientState?.queries?.length) return null; return ; } catch (error) { 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..454256366 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx @@ -45,7 +45,9 @@ export const BoardContentHeaderActions = () => { const [isEditMode] = useEditMode(); const board = useRequiredBoard(); const { hasChangeAccess } = useBoardPermissions(board); - const { data: demoMode, isLoading } = clientApi.info.isDemoMode.useQuery(); + // Fall back to read-only if the query has no data (e.g. errored) so we never expose + // edit/save/settings UI that would then fail server-side. + const { data: demoReadOnly = true, isLoading } = clientApi.info.isDemoReadOnly.useQuery(); if (!hasChangeAccess || isLoading) { return ; @@ -55,9 +57,9 @@ export const BoardContentHeaderActions = () => { <> {isEditMode && } - + - {!demoMode && ( + {!demoReadOnly && ( @@ -158,7 +160,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 +190,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 dbb81dc49..2388e2812 100644 --- a/apps/nextjs/src/app/[locale]/layout.tsx +++ b/apps/nextjs/src/app/[locale]/layout.tsx @@ -142,7 +142,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/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/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/widget-context-menu.tsx b/apps/nextjs/src/components/board/items/widget-context-menu.tsx index 415151a52..c3f3469cb 100644 --- a/apps/nextjs/src/components/board/items/widget-context-menu.tsx +++ b/apps/nextjs/src/components/board/items/widget-context-menu.tsx @@ -2,10 +2,10 @@ import type { MutableRefObject, ReactNode } from "react"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { Group, Menu, Switch, Text } from "@mantine/core"; +import { Group, Loader, Menu, Switch, Text } from "@mantine/core"; import { IconCopy, IconLayoutKanban, IconRefresh, IconSettings, IconTrash } from "@tabler/icons-react"; import type { QueryClient } from "@tanstack/react-query"; -import { useQueryClient } from "@tanstack/react-query"; +import { useIsFetching, useQueryClient } from "@tanstack/react-query"; import { clientApi } from "@homarr/api/client"; import { useSession } from "@homarr/auth/client"; @@ -50,8 +50,9 @@ export const WidgetContextMenu = ({ item, widgetStateRef, children }: WidgetCont const queryClient = useQueryClient(); const widgetQueryKey = useMemo(() => [["widget", item.kind]], [item.kind]); + const isWidgetFetching = useIsFetching({ queryKey: widgetQueryKey }) > 0; const handleRefetch = useCallback(() => { - void queryClient.invalidateQueries({ queryKey: widgetQueryKey }); + void queryClient.refetchQueries({ queryKey: widgetQueryKey, type: "all" }); }, [queryClient, widgetQueryKey]); const options = useMemo( @@ -223,11 +224,15 @@ export const WidgetContextMenu = ({ item, widgetStateRef, children }: WidgetCont <> {(toggleOptions.length > 0 || visibleWidgetActions.length > 0 || isEditMode) && } - } onClick={handleRefetch}> + : } + onClick={handleRefetch} + disabled={isWidgetFetching} + > {tMenu("refresh")} - + @@ -265,13 +270,24 @@ export const WidgetContextMenu = ({ item, widgetStateRef, children }: WidgetCont ); }; -const WidgetCacheAge = ({ queryClient, queryKey }: { queryClient: QueryClient; queryKey: unknown[] }) => { +const WidgetCacheAge = ({ + queryClient, + queryKey, + isFetching, +}: { + queryClient: QueryClient; + queryKey: unknown[]; + 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 }) diff --git a/deployments/prebuilt-debian/package.json b/deployments/prebuilt-debian/package.json index 0374dd9a6..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.18.0", - "pnpm": ">=11.9.0" + "pnpm": ">=11.10.0" }, - "packageManager": "pnpm@11.9.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 99aa947bf..6f281b0f3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -95,20 +95,27 @@ + + Bazarr +
+

Bazarr

+
+ + Beszel

Beszel

- - + + Coolify

Coolify

- - + + Dash.
@@ -149,15 +156,15 @@

Gotify

- - + + Home Assistant

Home
Assistant

- - + + iCal
@@ -198,15 +205,15 @@

Navidrome

- - + + Nextcloud

Nextcloud

- - + + ntfy
@@ -247,6 +254,13 @@

Paperless-ngx

+ + + + PatchMon +
+

PatchMon

+
@@ -254,8 +268,8 @@

PeaNUT

- - + + Pi-hole
@@ -289,8 +303,8 @@

qBittorrent

- - + + Radarr
@@ -303,8 +317,8 @@

Readarr

- - + + SABnzbd
@@ -338,6 +352,13 @@

Speedtest
Tracker

+ + + + Synology DiskStation +
+

Synology
DiskStation

+
@@ -352,8 +373,8 @@

Technitium
DNS

- - + + Tracearr
@@ -361,6 +382,13 @@
+ + Traefik +
+

Traefik

+
+ + Transmission
@@ -373,8 +401,8 @@

TrueNAS

- - + + Umami
diff --git a/package.json b/package.json index 2865dbdf5..69cd6b83e 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@semantic-release/release-notes-generator": "catalog:", "@testcontainers/redis": "catalog:", "@turbo/gen": "catalog:", + "@vitejs/plugin-react": "catalog:", "@vitest/coverage-v8": "catalog:", "@vitest/ui": "catalog:", @@ -62,9 +63,9 @@ }, "engines": { "node": ">=24.18.0", - "pnpm": ">=11.9.0" + "pnpm": ">=11.10.0" }, - "packageManager": "pnpm@11.9.0", + "packageManager": "pnpm@11.10.0", "dependencies": { "jsonpath-plus": "catalog:" } 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 b39e4a97d..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,6 +18,7 @@ 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"; @@ -28,6 +31,8 @@ export const mcpRouter = createTRPCRouter({ info: infoRouter, integration: integrationRouter, invite: inviteRouter, + bazarr: bazarrRouter, + serverSettings: serverSettingsRouter, beszel: beszelRouter, calendar: calendarRouter, dnsHole: dnsHoleRouter, @@ -35,6 +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/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/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/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/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/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 797de8609..66516b372 100644 --- a/packages/api/src/router/widgets/beszel.ts +++ b/packages/api/src/router/widgets/beszel.ts @@ -1,18 +1,24 @@ import { TRPCError } from "@trpc/server"; +import { observable } from "@trpc/server/observable"; import { z } from "zod/v4"; import { createLogger } from "@homarr/core/infrastructure/logs"; +import { createIntegrationAsync } from "@homarr/integrations"; +import type { LiveStatsEvent } from "@homarr/integrations/types"; import { beszelAlertsRequestHandler, beszelStatsRequestHandler, beszelSystemsRequestHandler, } 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({ @@ -26,8 +32,9 @@ 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.getDataAsync(); return { @@ -37,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, @@ -88,8 +88,9 @@ 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, @@ -115,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, @@ -197,8 +191,72 @@ export const beszelRouter = createTRPCRouter({ systemStats: [], containerStats: [], updatedAt: new Date(0), - error: error instanceof Error ? error.message : String(error), + error: errorMessage(error), }; } }), + + subscribeSystemStats: publicProcedure + .concat(createManyIntegrationMiddleware("query", "beszel", "mock")) + .input( + z.object({ + systemId: z.string(), + }), + ) + .subscription(({ ctx, input }) => { + return observable((emit) => { + const controller = new AbortController(); + let isActive = true; + + 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); + if (typeof instance.subscribeRealtimeMetrics === "function") { + await instance.subscribeRealtimeMetrics( + input.systemId, + (event) => { + if (isActive) 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 (isActive) { + emit.error( + error instanceof TRPCError + ? error + : new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: error instanceof Error ? error.message : String(error), + }), + ); + } + } + + if (isActive) { + emit.complete(); + } + })(); + + return () => { + isActive = false; + controller.abort(); + }; + }); + }), }); diff --git a/packages/api/src/router/widgets/downloads.ts b/packages/api/src/router/widgets/downloads.ts index 50aee350e..bdce2473f 100644 --- a/packages/api/src/router/widgets/downloads.ts +++ b/packages/api/src/router/widgets/downloads.ts @@ -49,6 +49,7 @@ export const downloadsRouter = createTRPCRouter({ await integrationInstance.pauseQueueAsync(); }), ); + downloadClientRequestHandler.invalidateCache(); }), pauseItem: protectedProcedure .concat(createDownloadClientIntegrationMiddleware("interact")) @@ -60,6 +61,7 @@ export const downloadsRouter = createTRPCRouter({ await integrationInstance.pauseItemAsync(input.item); }), ); + downloadClientRequestHandler.invalidateCache(); }), resume: protectedProcedure .meta({ @@ -77,6 +79,7 @@ export const downloadsRouter = createTRPCRouter({ await integrationInstance.resumeQueueAsync(); }), ); + downloadClientRequestHandler.invalidateCache(); }), resumeItem: protectedProcedure .concat(createDownloadClientIntegrationMiddleware("interact")) @@ -88,6 +91,7 @@ export const downloadsRouter = createTRPCRouter({ await integrationInstance.resumeItemAsync(input.item); }), ); + downloadClientRequestHandler.invalidateCache(); }), deleteItem: protectedProcedure .concat(createDownloadClientIntegrationMiddleware("interact")) @@ -99,5 +103,6 @@ export const downloadsRouter = createTRPCRouter({ await integrationInstance.deleteItemAsync(input.item, input.fromDisk); }), ); + downloadClientRequestHandler.invalidateCache(); }), }); diff --git a/packages/api/src/router/widgets/health-monitoring.ts b/packages/api/src/router/widgets/health-monitoring.ts index de49a1083..1957f459c 100644 --- a/packages/api/src/router/widgets/health-monitoring.ts +++ b/packages/api/src/router/widgets/health-monitoring.ts @@ -1,21 +1,31 @@ +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 settleIntegrationQueries(ctx.integrations, async (integration) => { const { data, timestamp } = await systemInfoRequestHandler.handler(integration, {}).getDataAsync(); @@ -27,6 +37,23 @@ export const healthMonitoringRouter = createTRPCRouter({ }; }); }), + 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: { diff --git a/packages/api/src/router/widgets/index.ts b/packages/api/src/router/widgets/index.ts index 046f422fe..5db62a93c 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,7 @@ 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)), secrets: lazy(() => import("./widget-secrets").then((mod) => mod.widgetSecretsRouter)), }); 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..782e4075a --- /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.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + return { + integrationId: integration.id, + ...data, + }; + }), + ); + return results; + }), +}); 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 b58fecd19..84e903627 100644 --- a/packages/api/src/router/widgets/releases.ts +++ b/packages/api/src/router/widgets/releases.ts @@ -45,7 +45,7 @@ export const releasesRouter = createTRPCRouter({ ) .query(async ({ ctx, input }) => { const tokensByProvider = new Map(); - let allowedRepoIds: Set | null = null; + const savedRepos = new Map(); if (input.itemId) { try { @@ -54,9 +54,15 @@ export const releasesRouter = createTRPCRouter({ await throwIfActionForbiddenAsync(ctx, eq(boards.id, item.boardId), "view"); const options = SuperJSON.parse>(item.options); - const repos = options.repositories as Array<{ id?: string }> | undefined; + const repos = options.repositories as + | Array<{ provider?: string; identifier: string; providerUrl?: string }> + | undefined; if (repos) { - allowedRepoIds = new Set(repos.map((r) => r.id).filter((id): id is string => Boolean(id))); + 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({ @@ -78,16 +84,16 @@ export const releasesRouter = createTRPCRouter({ return await Promise.all( input.repositories.map(async (repository) => { const repositoryId = repository.id ?? repository.identifier; + const savedRepo = savedRepos.get(`${repository.provider}:${repository.identifier}`); try { - const useToken = allowedRepoIds === null || allowedRepoIds.has(repositoryId); const response = await releasesRequestHandler .handler({ id: repositoryId, provider: repository.provider, identifier: repository.identifier, versionRegex: formatVersionFilterRegex(repository.versionFilter), - providerUrl: repository.providerUrl, - token: useToken ? tokensByProvider.get(repository.provider) : undefined, + providerUrl: savedRepo?.providerUrl ?? repository.providerUrl, + token: savedRepo ? tokensByProvider.get(repository.provider) : undefined, }) .getDataAsync(); 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/settle-integrations.ts b/packages/api/src/settle-integrations.ts index 9ae1b8f04..643a3b949 100644 --- a/packages/api/src/settle-integrations.ts +++ b/packages/api/src/settle-integrations.ts @@ -9,9 +9,14 @@ interface IntegrationLike { 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) => { @@ -24,6 +29,7 @@ export async function settleIntegrationQueries ({})); 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/trpc.ts b/packages/api/src/trpc.ts index 70abe4a3c..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 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/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/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/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/mysql/meta/0041_snapshot.json b/packages/db/migrations/mysql/meta/0041_snapshot.json new file mode 100644 index 000000000..1cbe65bde --- /dev/null +++ b/packages/db/migrations/mysql/meta/0041_snapshot.json @@ -0,0 +1,2583 @@ +{ + "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": "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": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} 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/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/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/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 new file mode 100644 index 000000000..9f0bc677e --- /dev/null +++ b/packages/db/migrations/sqlite/meta/0040_snapshot.json @@ -0,0 +1,2515 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "d76abdce-ba49-450b-81f0-621bcf581b90", + "prevId": "661fcf56-12b6-48a9-8d70-24032f0ec338", + "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": {} + }, + "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": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/packages/definitions/src/docker-integration-match.ts b/packages/definitions/src/docker-integration-match.ts index e7e10809b..a25fb8c44 100644 --- a/packages/definitions/src/docker-integration-match.ts +++ b/packages/definitions/src/docker-integration-match.ts @@ -31,8 +31,14 @@ const integrationAliases: Partial> = audiobookshelf: ["audiobookshelf"], navidrome: ["navidrome"], paperlessNgx: ["paperless-ngx", "paperless"], + patchmon: ["patchmon", "patch-mon"], + searchCh: ["search-ch"], + gitHubContainerRegistry: ["ghcr"], + linuxServerIO: ["linuxserver"], coolify: ["coolify"], truenas: ["truenas"], + bazarr: ["bazarr"], + synology: ["synology", "diskstation"], }; export const matchIntegrationKind = (search: string): IntegrationKind | null => { diff --git a/packages/definitions/src/docs/homarr-docs-sitemap.ts b/packages/definitions/src/docs/homarr-docs-sitemap.ts index 307f74428..22bc5f75b 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,6 +159,7 @@ export type HomarrDocumentationPath = | "/docs/integrations/archiveteam-warrior" | "/docs/integrations/aria2" | "/docs/integrations/audiobookshelf" + | "/docs/integrations/bazarr" | "/docs/integrations/beszel" | "/docs/integrations/coolify" | "/docs/integrations/dash-dot" @@ -192,6 +184,7 @@ export type HomarrDocumentationPath = | "/docs/integrations/opnsense" | "/docs/integrations/overseerr" | "/docs/integrations/paperless-ngx" + | "/docs/integrations/patchmon" | "/docs/integrations/peanut" | "/docs/integrations/pi-hole" | "/docs/integrations/plex" @@ -205,9 +198,11 @@ export type HomarrDocumentationPath = | "/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" @@ -230,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" @@ -249,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" @@ -260,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" @@ -289,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" @@ -312,10 +313,12 @@ export type HomarrDocumentationPath = | "/docs/integrations/ical" | "/docs/integrations/anchor" | "/docs/integrations/truenas" + | "/docs/integrations/synology" | "/docs/integrations/unraid" | "/docs/integrations/coolify" | "/docs/integrations/immich" | "/docs/integrations/paperless-ngx" + | "/docs/integrations/patchmon" | "/docs/integrations/tracearr" | "/docs/integrations/speedtest-tracker" | "/docs/integrations/uptime-kuma" @@ -325,6 +328,7 @@ export type HomarrDocumentationPath = | "/docs/integrations/gluetun" | "/docs/integrations/peanut" | "/docs/integrations/beszel" + | "/docs/integrations/traefik" | "/docs/widgets/clock" | "/docs/widgets/weather" | "/docs/widgets/app" @@ -343,6 +347,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" @@ -357,10 +362,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" @@ -372,4 +380,5 @@ export type HomarrDocumentationPath = | "/docs/widgets/beszel-system-grid" | "/docs/widgets/beszel-alerts" | "/docs/widgets/beszel-system-stats" + | "/docs/widgets/traefik" | "/docs/widgets/custom-api"; diff --git a/packages/definitions/src/docs/integration-doc-slugs.ts b/packages/definitions/src/docs/integration-doc-slugs.ts index ca19d4de6..41ea8eb33 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", @@ -36,10 +37,12 @@ export const integrationDocSlugs: Record = { ical: "ical", anchor: "anchor", truenas: "truenas", + synology: "synology", unraid: "unraid", coolify: "coolify", immich: "immich", paperlessNgx: "paperless-ngx", + patchmon: "patchmon", tracearr: "tracearr", speedtestTracker: "speedtest-tracker", uptimeKuma: "uptime-kuma", @@ -49,6 +52,7 @@ export const integrationDocSlugs: Record = { gluetun: "gluetun", peaNut: "peanut", beszel: "beszel", + traefik: "traefik", archiveTeamWarrior: null, mock: null, }; diff --git a/packages/definitions/src/docs/widget-doc-slugs.ts b/packages/definitions/src/docs/widget-doc-slugs.ts index 2e34a4843..8e51bbddb 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,5 +53,6 @@ export const widgetDocSlugs: Record = { beszelSystemGrid: "beszel-system-grid", beszelAlerts: "beszel-alerts", beszelSystemStats: "beszel-system-stats", + traefik: "traefik", customApi: "custom-api", }; diff --git a/packages/definitions/src/integration.ts b/packages/definitions/src/integration.ts index 15c94f80d..138a1c9f7 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"]], @@ -321,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"]], @@ -353,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"]], @@ -425,6 +453,14 @@ 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"]], @@ -453,6 +489,7 @@ export const integrationDefs = { "smartHomeServer", "ups", "uptimeMonitoring", + "mediaOrganizer", ], documentationUrl: null, }, @@ -518,6 +555,7 @@ export const integrationCategories = [ "dnsHole", "mediaService", "calendar", + "mediaOrganizer", "mediaSearch", "mediaRelease", "mediaRequest", @@ -545,6 +583,8 @@ export const integrationCategories = [ "documents", "mediaLibrary", "uptimeMonitoring", + "subtitleManager", + "reverseProxy", ] as const; export type IntegrationCategory = (typeof integrationCategories)[number]; diff --git a/packages/definitions/src/widget-integration-map.ts b/packages/definitions/src/widget-integration-map.ts index deb953781..1844fb662 100644 --- a/packages/definitions/src/widget-integration-map.ts +++ b/packages/definitions/src/widget-integration-map.ts @@ -20,19 +20,22 @@ 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"], "immich-serverStats": ["immich"], "immich-albumCarousel": ["immich"], paperlessNgx: ["paperlessNgx"], + patchmon: ["patchmon"], + bazarr: ["bazarr"], tracearr: ["tracearr"], speedtestTracker: ["speedtestTracker"], uptimeKuma: ["uptimeKuma"], @@ -40,6 +43,7 @@ export const widgetIntegrationSupport: Partial { diff --git a/packages/definitions/src/widget.ts b/packages/definitions/src/widget.ts index db2028082..c5c050b83 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", ] as const; export type WidgetKind = (typeof widgetKinds)[number]; @@ -56,4 +60,7 @@ export const widgetDefaultSizes: Partial 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 186ed2203..e9c4b35b2 100644 --- a/packages/integrations/src/beszel/beszel-integration.ts +++ b/packages/integrations/src/beszel/beszel-integration.ts @@ -20,6 +20,7 @@ import type { BeszelSystemdService, BeszelSystemStatsRecord, CreateAlertInput, + LiveStatsEvent, PocketBaseListResponse, UpdateAlertInput, } from "./beszel-types"; @@ -284,6 +285,125 @@ export class BeszelIntegration extends Integration { }); } + /** + * Subscribe to real-time system and container stats via PocketBase SSE. + * Connects to the PocketBase Realtime API and parses incoming SSE events, + * filtering by systemId. This provides live-updating metrics (≤1s latency + * from agent) instead of polling the REST API every 5s. + */ + public async subscribeRealtimeMetrics( + systemId: string, + onMessage: (event: LiveStatsEvent) => void, + signal: AbortSignal, + ): Promise { + const session = await this.authenticateAsync(); + const realtimeUrl = this.url("/api/realtime"); + + logger.debug("Opening Beszel SSE connection for realtime metrics", { + integrationId: this.integration.id, + systemId, + url: realtimeUrl.pathname, + }); + + const response = await fetchWithTrustedCertificatesAsync(realtimeUrl, { + headers: { Authorization: session.token }, + signal, + }); + + if (!response.ok) { + logger.warn("Beszel SSE connection failed", { + integrationId: this.integration.id, + systemId, + status: response.status, + }); + throw new ResponseError(response); + } + + const reader = response.body?.getReader(); + if (!reader) throw new Error("No response body in SSE stream"); + + const decoder = new TextDecoder(); + let buffer = ""; + let subscribed = false; + + const processLine = async (line: string) => { + if (!line.startsWith("data: ")) return; + + try { + const parsed = JSON.parse(line.slice(6)) as Record; + + // PB_CONNECT delivers the clientId — use it to POST our subscriptions + if (!subscribed && typeof parsed.clientId === "string") { + subscribed = true; + const subscribeResponse = await fetchWithTrustedCertificatesAsync(realtimeUrl, { + method: "POST", + headers: { + Authorization: session.token, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + clientId: parsed.clientId, + subscriptions: ["system_stats", "container_stats"], + }), + signal, + }); + + if (!subscribeResponse.ok) { + logger.warn("Beszel SSE subscription POST failed", { + integrationId: this.integration.id, + systemId, + status: subscribeResponse.status, + }); + throw new ResponseError(subscribeResponse); + } + + logger.debug("Beszel SSE subscribed to collections", { + integrationId: this.integration.id, + systemId, + clientId: parsed.clientId, + }); + return; + } + + const record = parsed.record as Record | undefined; + if (!record || record.system !== systemId) return; + + if (Array.isArray(record.stats)) { + onMessage({ + type: "container_stats", + record: record as unknown as BeszelContainerStatsRecord, + }); + } else if (record.stats && typeof record.stats === "object") { + onMessage({ + type: "system_stats", + record: record as unknown as BeszelSystemStatsRecord, + }); + } + } catch (error) { + if (error instanceof ResponseError) throw error; + // skip malformed SSE data lines + } + }; + + try { + while (true) { + 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) { + await processLine(line); + } + } + } finally { + reader.releaseLock(); + logger.debug("Beszel SSE connection closed", { integrationId: this.integration.id, systemId }); + } + } + protected async testingAsync(input: IntegrationTestingInput): Promise { const response = await input.fetchAsync(this.url("/api/collections/users/auth-with-password"), { method: "POST", diff --git a/packages/integrations/src/beszel/beszel-types.ts b/packages/integrations/src/beszel/beszel-types.ts index e5c6566a5..1abbc6564 100644 --- a/packages/integrations/src/beszel/beszel-types.ts +++ b/packages/integrations/src/beszel/beszel-types.ts @@ -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; 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/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/index.ts b/packages/integrations/src/index.ts index 8e20bf745..f172cec36 100644 --- a/packages/integrations/src/index.ts +++ b/packages/integrations/src/index.ts @@ -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"; @@ -41,6 +42,8 @@ export { NavidromeIntegration } from "./navidrome/navidrome-integration"; export { UptimeKumaIntegration } from "./uptime-kuma/uptime-kuma-integration"; export { UmamiIntegration } from "./umami/umami-integration"; export { PeaNutIntegration } from "./peanut/peanut-integration"; +export { BazarrIntegration } from "./bazarr/bazarr-integration"; +export { TraefikIntegration } from "./traefik/traefik-integration"; // Types export type { IntegrationInput } from "./base/integration"; @@ -67,6 +70,7 @@ export type { 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, @@ -80,6 +84,8 @@ export type { AudiobookshelfDashboardData } from "./audiobookshelf/audiobookshel 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, 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/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..a83347553 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; @@ -332,7 +333,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 +349,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/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/synology/synology-client.ts b/packages/integrations/src/synology/synology-client.ts new file mode 100644 index 000000000..54de015d7 --- /dev/null +++ b/packages/integrations/src/synology/synology-client.ts @@ -0,0 +1,411 @@ +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 { + AUTH_API_NAME, + INFO_API_NAME, + REQUEST_TIMEOUT_MS, + SESSION_ERROR_CODES, + synologyApiInfoResponseSchema, + synologyAuthResponseSchema, + synologyEnvelopeSchema, + synologyLegacyStorageDataSchema, + synologySmartInfoDataSchema, + synologyStorageLoadInfoDataSchema, + synologyStorageV2DataSchema, + synologySystemInfoDataSchema, + synologySystemStatusDataSchema, + synologyUpgradeCheckDataSchema, + synologyUpgradeStatusDataSchema, + synologyUtilizationDataSchema, + type SynologyDiskRecord, + type SynologyVolumeRecord, +} 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..07153b1e4 --- /dev/null +++ b/packages/integrations/src/synology/test/synology-integration.spec.ts @@ -0,0 +1,585 @@ +// @vitest-environment node + +import { Response } from "undici"; +import { beforeEach, describe, expect, test, vi } from "vitest"; + +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"; + +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?: typeof utilizationResponse; + storageV2?: typeof storageV2Response; + 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< + typeof fetchWithTrustedCertificatesAsync + >; + } + + 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 273b5b2d2..3d9186b3e 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"; @@ -24,3 +26,4 @@ export * from "./paperless-ngx/paperless-ngx-types"; export * from "./umami/umami-types"; export * from "./gluetun/gluetun-types"; export * from "./beszel/beszel-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/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/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 4a60a3a3a..a6619e6ef 100644 --- a/packages/request-handler/src/beszel.ts +++ b/packages/request-handler/src/beszel.ts @@ -122,6 +122,9 @@ export const beszelStatsRequestHandler = createIntegrationRequestHandler< "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 }; diff --git a/packages/request-handler/src/docker.ts b/packages/request-handler/src/docker.ts index 12a78c05a..a238b4a3c 100644 --- a/packages/request-handler/src/docker.ts +++ b/packages/request-handler/src/docker.ts @@ -300,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( () => ({ @@ -336,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/health-monitoring.ts b/packages/request-handler/src/health-monitoring.ts index 604fff9da..959d3e0ed 100644 --- a/packages/request-handler/src/health-monitoring.ts +++ b/packages/request-handler/src/health-monitoring.ts @@ -6,7 +6,7 @@ import { createIntegrationRequestHandler } from "./lib/integration-request-handl export const systemInfoRequestHandler = createIntegrationRequestHandler< SystemHealthMonitoring, - Exclude, "proxmox" | "coolify" | "beszel">, + Exclude, "proxmox" | "coolify" | "beszel" | "patchmon">, Record >({ async requestAsync(integration, _input) { diff --git a/packages/request-handler/src/lib/integration-request-handler.ts b/packages/request-handler/src/lib/integration-request-handler.ts index d2fa0f1a4..e60be1fb8 100644 --- a/packages/request-handler/src/lib/integration-request-handler.ts +++ b/packages/request-handler/src/lib/integration-request-handler.ts @@ -12,6 +12,7 @@ type IntegrationOfKind = Omit> { requestAsync: (integration: IntegrationOfKind, input: TInput) => Promise; + cacheTtlMs?: number; } export const createIntegrationRequestHandler = < @@ -20,11 +21,24 @@ export const createIntegrationRequestHandler = < TInput extends Record, >( options: Options, -) => ({ - handler: (integration: IntegrationOfKind, itemOptions: TInput) => { - const inner = createRequestHandler({ - requestAsync: async (input) => options.requestAsync(integration, input.options), - }); - return inner.handler({ integrationId: integration.id, options: itemOptions }); - }, -}); +) => { + 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 index bafcc8c8a..f733078ee 100644 --- a/packages/request-handler/src/lib/request-handler.ts +++ b/packages/request-handler/src/lib/request-handler.ts @@ -22,6 +22,10 @@ export const createRequestHandler = >>(); return { + invalidateCache: () => { + cache.clear(); + inflight.clear(); + }, handler: (input: TInput) => ({ async getDataAsync(): Promise<{ data: TData; timestamp: Date }> { const ttl = options.cacheTtlMs ?? DEFAULT_TTL_MS; 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/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 index f4a647668..d4a9dd2b3 100644 --- a/packages/request-handler/src/release-providers.ts +++ b/packages/request-handler/src/release-providers.ts @@ -134,7 +134,7 @@ const getGithubApi = (baseUrl: string, userAgent: string, token?: string) => baseUrl, auth: token, request: { fetch: fetchWithTrustedCertificatesAsync }, - throttle: { enabled: false }, + throttle: { enabled: true }, userAgent, }); 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/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/translation/src/lang/ca.json b/packages/translation/src/lang/ca.json index c5bf0f0d2..e227775fb 100644 --- a/packages/translation/src/lang/ca.json +++ b/packages/translation/src/lang/ca.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/cn.json b/packages/translation/src/lang/cn.json index d7095d953..aa0baf940 100644 --- a/packages/translation/src/lang/cn.json +++ b/packages/translation/src/lang/cn.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "显示文件系统信息" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "默认标签" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "显示背景使用栏" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/cr.json b/packages/translation/src/lang/cr.json index 830af4676..3a4ae4319 100644 --- a/packages/translation/src/lang/cr.json +++ b/packages/translation/src/lang/cr.json @@ -2284,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" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "crwdns3842:0crwdne3842:0" + }, + "visibleStorageVolumes": { + "label": "crwdns5747:0crwdne5747:0", + "description": "crwdns5749:0crwdne5749:0", + "placeholder": "crwdns5751:0crwdne5751:0" } }, "status": { diff --git a/packages/translation/src/lang/cs.json b/packages/translation/src/lang/cs.json index 71fd83bee..016c40061 100644 --- a/packages/translation/src/lang/cs.json +++ b/packages/translation/src/lang/cs.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "Zobrazit informace o souborovém systému" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/da.json b/packages/translation/src/lang/da.json index 8d8582362..bbb6da4c6 100644 --- a/packages/translation/src/lang/da.json +++ b/packages/translation/src/lang/da.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "Vis information om filsystemet" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Standard fane" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "Vis forbrugslinje i baggrunden" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/de-CH.json b/packages/translation/src/lang/de-CH.json index 59ece15da..4c9c8f313 100644 --- a/packages/translation/src/lang/de-CH.json +++ b/packages/translation/src/lang/de-CH.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "Dateisystem Info anzeigen" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Standard Tab" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/de.json b/packages/translation/src/lang/de.json index 03d8b7934..086b3ea36 100644 --- a/packages/translation/src/lang/de.json +++ b/packages/translation/src/lang/de.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "Dateisystem Info anzeigen" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Standard Tab" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "Zeige Hintergrundnutzungsleiste" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/el.json b/packages/translation/src/lang/el.json index dfcc2c11b..3e64041dd 100644 --- a/packages/translation/src/lang/el.json +++ b/packages/translation/src/lang/el.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "Εμφάνιση Πληροφοριών Συστήματος Αρχείων" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/en-gb.json b/packages/translation/src/lang/en-gb.json index a80b7cf55..15ed8c7fe 100644 --- a/packages/translation/src/lang/en-gb.json +++ b/packages/translation/src/lang/en-gb.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "Show Filesystem Info" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Default tab" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index f4cbf6f98..c314dd1ef 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": { @@ -2284,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" }, @@ -2749,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", @@ -3088,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", @@ -3331,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", @@ -3412,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", @@ -3538,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": { @@ -3747,6 +4031,9 @@ }, "showDockerNetwork": { "label": "Show Docker network chart" + }, + "showStorage": { + "label": "Show storage chart" } }, "selectSystem": "Select system", @@ -3782,6 +4069,11 @@ "read": "Read", "write": "Write" }, + "storage": { + "title": "Storage", + "subtitle": "Per-filesystem disk usage", + "series": "Used" + }, "network": { "title": "Bandwidth", "subtitle": "Network traffic of public interfaces", diff --git a/packages/translation/src/lang/es.json b/packages/translation/src/lang/es.json index 0a76d6115..a64ae6915 100644 --- a/packages/translation/src/lang/es.json +++ b/packages/translation/src/lang/es.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "Mostrar información del sistema de archivos" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Seleccionar pestaña predeterminada" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "Mostrar la barra de uso en segundo plano" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/et.json b/packages/translation/src/lang/et.json index 29a70aea5..23da3299c 100644 --- a/packages/translation/src/lang/et.json +++ b/packages/translation/src/lang/et.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/fi.json b/packages/translation/src/lang/fi.json index 32d449dc2..7388d9322 100644 --- a/packages/translation/src/lang/fi.json +++ b/packages/translation/src/lang/fi.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/fr.json b/packages/translation/src/lang/fr.json index 359c9caa6..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,23 +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": "", - "vpn": "", - "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": { @@ -1298,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" } } }, @@ -1313,7 +1313,7 @@ "description": "" }, "navidrome": { - "name": "", + "name": "Navidrome", "description": "" }, "uptimeKuma": { @@ -1385,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", @@ -1407,20 +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.", - "headerLoading": "", - "noResults": "", - "error": "" + "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" } @@ -1468,7 +1468,7 @@ "switchToDarkMode": "Activer le mode sombre", "switchToLightMode": "Activer le mode clair", "management": "Administration", - "docker": "", + "docker": "Docker", "preferences": "Vos préférences", "logout": "Se déconnecter", "login": "Connexion", @@ -1578,7 +1578,7 @@ "label": { "create": "Nouvelle catégorie", "changePosition": "Modifier la position", - "settings": "" + "settings": "Réglages" } }, "openAllInNewTabs": { @@ -1599,9 +1599,9 @@ "menu": { "label": { "settings": "Paramètres", - "refresh": "", - "options": "", - "actions": "" + "refresh": "Actualiser", + "options": "Options", + "actions": "Actions" } }, "create": { @@ -1629,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", @@ -1661,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", @@ -1746,7 +1746,7 @@ "label": "Ouvrir dans un nouvel onglet" }, "withBorder": { - "label": "" + "label": "Afficher la bordure" }, "items": { "label": "Signets", @@ -1759,7 +1759,7 @@ "description": "", "option": { "showBroadcastMessage": { - "label": "" + "label": "Afficher les messages de diffusion" } } }, @@ -1899,7 +1899,7 @@ }, "timetable": { "name": "", - "description": "", + "description": "Affiche les heures de départ pour une station", "title": "", "noStation": "", "option": { @@ -1997,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", @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "Afficher les infos sur le système de fichiers" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Onglet par défaut" }, @@ -2430,27 +2435,27 @@ "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": "" @@ -2461,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" @@ -2474,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", @@ -2522,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": { @@ -2597,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": "" } }, @@ -2705,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": "", @@ -2717,7 +2722,7 @@ "total": "", "option": { "showInboxRatio": { - "label": "", + "label": "Afficher le ratio de la boîte de réception", "description": "" }, "showInboxRing": { @@ -3158,7 +3163,7 @@ "label": "Fournisseur" }, "providerUrl": { - "label": "" + "label": "URL du fournisseur" }, "identifier": { "label": "Identifiant", @@ -3209,11 +3214,11 @@ "tooltip": "Le fournisseur n'a pas pu être analysé, veuillez le définir manuellement après l'importation des images" }, "tokens": { - "label": "", - "token": "", - "configured": "", - "notConfigured": "", - "save": "" + "label": "Tokens du fournisseur", + "token": "Token", + "configured": "Token configuré", + "notConfigured": "Aucun token défini", + "save": "Sauvegarder" } } }, @@ -3236,9 +3241,9 @@ "invalidIdentifier": "Identifiant non valide", "noMatchingVersion": "Aucune version correspondante trouvée", "noReleasesFound": "Aucune publication trouvée", - "noProviderSelected": "", + "noProviderSelected": "Aucun fournisseur sélectionné", "noProviderResponse": "Aucune réponse du fournisseur", - "unexpected": "" + "unexpected": "Erreur de fournisseur inattendue" } } }, @@ -3334,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": "", @@ -3386,7 +3391,7 @@ }, "showTotalMonitors": { "label": "", - "description": "" + "description": "Afficher le nombre total de moniteurs configurés" }, "showUpCount": { "label": "", @@ -3401,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": { @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "Afficher la barre d'utilisation en arrière-plan" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/he.json b/packages/translation/src/lang/he.json index da3b62e8b..f2e575553 100644 --- a/packages/translation/src/lang/he.json +++ b/packages/translation/src/lang/he.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "הצג מידע על מערכת הקבצים" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "כרטיסיית ברירת מחדל" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/hr.json b/packages/translation/src/lang/hr.json index a300ca258..55ebb428b 100644 --- a/packages/translation/src/lang/hr.json +++ b/packages/translation/src/lang/hr.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/hu.json b/packages/translation/src/lang/hu.json index 0ac21e379..f9dbaf927 100644 --- a/packages/translation/src/lang/hu.json +++ b/packages/translation/src/lang/hu.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "Fájlrendszer-információk megjelenítése" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Alapértelmezett fül" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "Háttérhasználati sáv megjelenítése" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/it.json b/packages/translation/src/lang/it.json index 1280ff538..63629d558 100644 --- a/packages/translation/src/lang/it.json +++ b/packages/translation/src/lang/it.json @@ -1599,7 +1599,7 @@ "menu": { "label": { "settings": "Impostazioni", - "refresh": "", + "refresh": "Aggiorna", "options": "Opzioni", "actions": "Azioni" } @@ -1904,7 +1904,7 @@ "noStation": "Nessuna stazione selezionata", "option": { "baseUrl": { - "label": "" + "label": "Url Search.ch" }, "station": { "label": "Stazione" @@ -2284,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" }, @@ -2791,8 +2796,8 @@ "description": "Visualizza la data dell'immagine e l'indice corrente nella parte inferiore della presentazione" }, "randomizePhotos": { - "label": "", - "description": "" + "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", @@ -3158,7 +3163,7 @@ "label": "Provider" }, "providerUrl": { - "label": "" + "label": "URL Provider" }, "identifier": { "label": "Identificatore", @@ -3209,10 +3214,10 @@ "tooltip": "Impossibile analizzare il provider, impostarlo manualmente dopo aver importato le immagini" }, "tokens": { - "label": "", - "token": "", - "configured": "", - "notConfigured": "", + "label": "Token Provider", + "token": "Token", + "configured": "Token configurato", + "notConfigured": "Nessun token impostato", "save": "Salva" } } @@ -3236,9 +3241,9 @@ "invalidIdentifier": "Identificatore non valido", "noMatchingVersion": "Nessuna versione corrispondente trovata", "noReleasesFound": "Nessun rilascio trovato", - "noProviderSelected": "", + "noProviderSelected": "Nessun provider selezionato", "noProviderResponse": "Nessuna risposta dal provider", - "unexpected": "" + "unexpected": "Errore provider inaspettato" } } }, @@ -3538,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": { diff --git a/packages/translation/src/lang/ja.json b/packages/translation/src/lang/ja.json index 7cb5087c5..e6018ccbd 100644 --- a/packages/translation/src/lang/ja.json +++ b/packages/translation/src/lang/ja.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "ファイルシステム情報を表示" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "デフォルトのタブ" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "バックグラウンドの使用状況バーを表示" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/ko.json b/packages/translation/src/lang/ko.json index 7c8d11572..e665078ee 100644 --- a/packages/translation/src/lang/ko.json +++ b/packages/translation/src/lang/ko.json @@ -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,34 +821,34 @@ "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": "", + "apiKey": "API 키/토큰", + "username": "사용자명", "password": "비밀번호", "savedPlaceholder": "••••••• (저장됨, 유지하려면 공백)" }, @@ -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,14 +1021,14 @@ "mediaMonitoring": "모니터링", "speedtest": "속도 테스트", "analytics": "분석", - "vpn": "", + "vpn": "VPN", "mediaLibrary": "미디어 라이브러리", "uptimeMonitoring": "업타임 모니터링" }, "testConnection": { "action": { - "create": "", - "edit": "" + "create": "연결 테스트 및 생성", + "edit": "연결 테스트 및 저장" }, "error": { "common": { @@ -1037,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": { @@ -1101,7 +1101,7 @@ }, "notification": { "success": { - "title": "", + "title": "신뢰하는 인증서", "message": "" }, "error": { @@ -1127,16 +1127,16 @@ } }, "details": { - "title": "", + "title": "세부 사항", "description": "", "content": { - "action": "", + "action": "항목 보기", "title": "" } } }, "request": { - "title": "", + "title": "요청 오류", "description": { "connection": { "hostUnreachable": "", @@ -1175,7 +1175,7 @@ "message": "" }, "commonError": { - "title": "", + "title": "연결 실패", "message": "" }, "badRequest": { @@ -1229,7 +1229,7 @@ } }, "secrets": { - "title": "", + "title": "비밀 구문", "lastUpdated": "", "notSet": { "label": "", @@ -1247,19 +1247,19 @@ "kind": { "username": { "label": "사용자 이름", - "newLabel": "" + "newLabel": "새 사용자 이름" }, "apiKey": { "label": "", - "newLabel": "" + "newLabel": "새 API 키" }, "password": { "label": "비밀번호", - "newLabel": "" + "newLabel": "새 패스워드" }, "tokenId": { "label": "", - "newLabel": "" + "newLabel": "새 토큰 ID" }, "realm": { "label": "", @@ -1267,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": "", @@ -1527,7 +1527,7 @@ "section": { "dynamic": { "action": { - "create": "", + "create": "새 동적 섹션", "remove": "" }, "option": { @@ -1553,17 +1553,17 @@ } }, "action": { - "create": "", + "create": "새 카테고리", "edit": "", "remove": "", "moveUp": "위로 이동", "moveDown": "아래로 이동", "createAbove": "", "createBelow": "", - "openAllInNewTabs": "" + "openAllInNewTabs": "탭에서 모두 열기" }, "create": { - "title": "", + "title": "새 카테고리", "submit": "" }, "remove": { @@ -1576,32 +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": "설정", - "refresh": "", - "options": "", - "actions": "" + "refresh": "새로고침", + "options": "옵션", + "actions": "동작" } }, "create": { @@ -1610,7 +1610,7 @@ "addToBoard": "" }, "moveResize": { - "title": "", + "title": "항목 이동 / 크기변경", "field": { "width": { "label": "너비" @@ -1619,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": "고급 옵션", @@ -1837,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을 사용하여 날짜 형식 지정 (다른 옵션 무시)" } } }, @@ -1913,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": "계속 편집" } } }, @@ -2010,7 +2010,7 @@ "saveFailed": "" }, "iframe": { - "name": "", + "name": "iFrame", "description": "인터넷에서 콘텐츠를 퍼옵니다. 일부 웹사이트는 액세스를 제한할 수 있습니다.", "option": { "embedUrl": { @@ -2041,12 +2041,12 @@ "label": "지리적 위치 허용" }, "allowModals": { - "label": "" + "label": "모달 허용" } }, "error": { - "noUrl": "", - "unsupportedProtocol": "", + "noUrl": "iFrame URL 없음", + "unsupportedProtocol": "URL이 지원되지 않는 프로토콜을 사용합니다. ({supportedProtocols}) 중 하나를 사용하십시오", "noBrowerSupport": "브라우저가 iframe을 지원하지 않습니다. 브라우저를 업데이트하세요." } }, @@ -2161,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": "모니터링되지 않음 표시" } } }, @@ -2190,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": "맑음", @@ -2259,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": "노드" } } } @@ -2420,9 +2425,9 @@ } }, "vpn": { - "name": "", - "description": "", - "serviceUnavailable": "", + "name": "VPN", + "description": "VPN 연결 개요", + "serviceUnavailable": "VPN 연결되지 않음", "option": {} }, "ups": { @@ -2671,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": "목표 코덱" } } } @@ -2800,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": { @@ -3246,7 +3251,7 @@ "option": {}, "card": { "vpn": { - "countConnected": "" + "countConnected": "{count}개 연결" } }, "error": { @@ -3328,9 +3333,9 @@ } }, "notifications": { - "name": "", - "description": "", - "noItems": "", + "name": "알림", + "description": "통합에서 알림 기록 표시", + "noItems": "표시할 알림이 없습니다.", "option": {} }, "speedtestTracker": { @@ -3373,11 +3378,11 @@ } }, "uptimeKuma": { - "name": "", - "description": "", + "name": "Uptime Kuma", + "description": "Uptime Kuma 상태 페이지에서 업타임 통계를 모니터링합니다", "option": { "showAverageUptime": { - "label": "", + "label": "평균 업타임 표시", "description": "" }, "showUptimeRing": { @@ -3480,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": { @@ -3925,7 +3935,7 @@ "placeholder": "로컬 이미지 검색을 위한 타이핑 시작", "group": { "your": "내 이미지", - "other": "" + "other": "기타 이미지" } }, "backgroundImageAttachment": { @@ -3963,7 +3973,7 @@ } }, "backgroundImageSize": { - "label": "", + "label": "배경 이미지 크기", "option": { "cover": { "label": "", @@ -3982,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": { @@ -4021,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": "레이아웃 추가" } } }, @@ -4047,10 +4057,10 @@ "title": "배경" }, "appearance": { - "title": "" + "title": "모양" }, "customCss": { - "title": "" + "title": "사용자 지정 css" }, "behavior": { "title": "동작" @@ -5015,7 +5025,7 @@ } }, "refresh": { - "label": "", + "label": "새로고침", "notification": { "success": { "title": "", diff --git a/packages/translation/src/lang/lt.json b/packages/translation/src/lang/lt.json index 826affd08..f3f3fce9b 100644 --- a/packages/translation/src/lang/lt.json +++ b/packages/translation/src/lang/lt.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/lv.json b/packages/translation/src/lang/lv.json index bc94f9345..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": "", @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "Rādīt failu sistēmas informāciju" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/nl.json b/packages/translation/src/lang/nl.json index 473819563..7d1e94f4d 100644 --- a/packages/translation/src/lang/nl.json +++ b/packages/translation/src/lang/nl.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "Bestandssysteem info weergeven" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Standaard tab" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/no.json b/packages/translation/src/lang/no.json index e387d198c..595f6b71e 100644 --- a/packages/translation/src/lang/no.json +++ b/packages/translation/src/lang/no.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "Vis filsysteminfo" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Standard fane" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "Vis bakgrunnsbrukslinje" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/pl.json b/packages/translation/src/lang/pl.json index 59bbe00ab..31d301ceb 100644 --- a/packages/translation/src/lang/pl.json +++ b/packages/translation/src/lang/pl.json @@ -1599,7 +1599,7 @@ "menu": { "label": { "settings": "Ustawienia", - "refresh": "", + "refresh": "Odśwież", "options": "Opcje", "actions": "Akcje" } @@ -1904,7 +1904,7 @@ "noStation": "Nie wybrano stacji", "option": { "baseUrl": { - "label": "" + "label": "Szukaj adresu URL" }, "station": { "label": "Stacja" @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "Pokaż informacje o systemie plików" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Domyślna karta" }, @@ -2791,8 +2796,8 @@ "description": "Wyświetl datę zdjęcia oraz bieżący indeks na dole pokazu slajdów" }, "randomizePhotos": { - "label": "", - "description": "" + "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", @@ -3158,7 +3163,7 @@ "label": "Dostawca" }, "providerUrl": { - "label": "" + "label": "URL dostawcy" }, "identifier": { "label": "Identyfikator", @@ -3209,11 +3214,11 @@ "tooltip": "Nie udało się rozpoznać dostawcy. Ustaw go ręcznie po zaimportowaniu obrazów." }, "tokens": { - "label": "", - "token": "", - "configured": "", - "notConfigured": "", - "save": "" + "label": "Tokeny dostawcy", + "token": "Token", + "configured": "Token skonfigurowany", + "notConfigured": "Brak ustawionych tokenów", + "save": "Zapisz" } } }, @@ -3236,9 +3241,9 @@ "invalidIdentifier": "Nieprawidłowy identyfikator", "noMatchingVersion": "Nie znaleziono pasującej wersji", "noReleasesFound": "Nie znaleziono wydań!", - "noProviderSelected": "", + "noProviderSelected": "Nie wybrano dostawcy", "noProviderResponse": "Brak odpowiedzi od dostawcy", - "unexpected": "" + "unexpected": "Nieoczekiwany błąd dostawcy" } } }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "Pokaż pasek wykorzystania w tle" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/pt-br.json b/packages/translation/src/lang/pt-br.json index 433db0f3f..1d8b6329e 100644 --- a/packages/translation/src/lang/pt-br.json +++ b/packages/translation/src/lang/pt-br.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "Mostrar informações do sistema de arquivos" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Aba padrão" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "Exibir barra de uso em segundo plano" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/pt.json b/packages/translation/src/lang/pt.json index eb490a8ba..ab6a20c1d 100644 --- a/packages/translation/src/lang/pt.json +++ b/packages/translation/src/lang/pt.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/ro.json b/packages/translation/src/lang/ro.json index cc5b26726..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": "" } }, @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "Afișare informații despre sistemul de fișiere" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/ru.json b/packages/translation/src/lang/ru.json index e23ac57ab..62b918327 100644 --- a/packages/translation/src/lang/ru.json +++ b/packages/translation/src/lang/ru.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "Показать информацию о файловой системе" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Вкладка по умолчанию" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "Показывать фоновую шкалу использования" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/sk.json b/packages/translation/src/lang/sk.json index 5336333d8..a7fbce04f 100644 --- a/packages/translation/src/lang/sk.json +++ b/packages/translation/src/lang/sk.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "Zobraziť informácie o súborovom systéme" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Predvolená karta" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/sl.json b/packages/translation/src/lang/sl.json index 17a586641..6678f65bd 100644 --- a/packages/translation/src/lang/sl.json +++ b/packages/translation/src/lang/sl.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/sv.json b/packages/translation/src/lang/sv.json index 0f84ef06f..7b43c6234 100644 --- a/packages/translation/src/lang/sv.json +++ b/packages/translation/src/lang/sv.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "Visa information om filsystemet" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/tr.json b/packages/translation/src/lang/tr.json index 59b5bf87e..5755a9519 100644 --- a/packages/translation/src/lang/tr.json +++ b/packages/translation/src/lang/tr.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "Dosya Sistemi Bilgilerini Göster" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "Varsayılan sekme" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "Show background usage bar" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/uk.json b/packages/translation/src/lang/uk.json index 5f7283e5e..b77603f3d 100644 --- a/packages/translation/src/lang/uk.json +++ b/packages/translation/src/lang/uk.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -3132,7 +3137,7 @@ "label": "" }, "showOnlyIcon": { - "label": "" + "label": "Показати тільки іконку" }, "topReleases": { "label": "", @@ -3229,7 +3234,7 @@ "openReleasePage": "", "releaseDescription": "", "projectDescription": "", - "created": "", + "created": "Створено", "error": { "label": "", "messages": { @@ -3350,7 +3355,7 @@ "label": "" } }, - "latestResult": "", + "latestResult": "Останній результат", "averages": "", "tests": "", "recentResults": "", @@ -3437,7 +3442,7 @@ "description": "" }, "maxNowPlayingItems": { - "label": "", + "label": "Максимум елементів для відтворення", "description": "" }, "showLibraryCount": { @@ -3532,12 +3537,17 @@ "label": "" }, "free": { - "label": "" + "label": "Відсоток вільного простору (напр., 24% вільно)" } } }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { @@ -3587,19 +3597,19 @@ "label": "" }, "showGpu": { - "label": "" + "label": "Показати графічний процесор" }, "showLoadAvg": { - "label": "" + "label": "Показати середній рівень навантаження" }, "showNet": { - "label": "" + "label": "Показати мережу" }, "showTemp": { - "label": "" + "label": "Показувати температуру" }, "showBattery": { - "label": "" + "label": "Показувати заряд батереї" }, "showServices": { "label": "" @@ -3613,7 +3623,7 @@ }, "noRecords": "", "error": { - "internalServerError": "" + "internalServerError": "Не вдається підключитися до Beszel. Перевірте параметри інтеграції." }, "column": { "system": "", @@ -3665,7 +3675,7 @@ "label": "" }, "showUptime": { - "label": "" + "label": "Показати час роботи" }, "showAgent": { "label": "" @@ -3686,7 +3696,7 @@ "net": "", "temp": "", "battery": "", - "services": "", + "services": "Служби", "uptime": "", "agent": "" } @@ -3769,12 +3779,12 @@ "title": "", "subtitle": "", "series": "", - "cache": "" + "cache": "Кеш/Баффери" }, "disk": { "title": "", "subtitle": "", - "series": "" + "series": "Використано" }, "diskIO": { "title": "", @@ -3971,7 +3981,7 @@ }, "contain": { "label": "", - "description": "" + "description": "Масштабує зображення максимально великою в межах контейнера без обрізання або розтягування зображення." } } }, @@ -4286,7 +4296,7 @@ "private": "Ця дошка є приватною" }, "noResults": { - "title": "" + "title": "У вас ще немає дошок" }, "modal": { "createBoard": { @@ -4655,7 +4665,7 @@ "table": { "header": { "id": "ІДЕНТИФІКАТОР", - "createdBy": "", + "createdBy": "Створено", "actions": "" } } diff --git a/packages/translation/src/lang/vi.json b/packages/translation/src/lang/vi.json index 8febe135e..36207f3d7 100644 --- a/packages/translation/src/lang/vi.json +++ b/packages/translation/src/lang/vi.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { diff --git a/packages/translation/src/lang/zh.json b/packages/translation/src/lang/zh.json index 10ae7d3a8..bfa7bcc0f 100644 --- a/packages/translation/src/lang/zh.json +++ b/packages/translation/src/lang/zh.json @@ -2284,6 +2284,11 @@ "fileSystem": { "label": "顯示檔案系統訊息" }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" + }, "defaultTab": { "label": "預設頁面" }, @@ -3538,6 +3543,11 @@ }, "showBackgroundBar": { "label": "" + }, + "visibleStorageVolumes": { + "label": "", + "description": "", + "placeholder": "" } }, "status": { 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 c7be65262..3d575bfb0 100644 --- a/packages/widgets/src/_inputs/common.tsx +++ b/packages/widgets/src/_inputs/common.tsx @@ -11,7 +11,7 @@ export interface CommonWidgetInputProps { 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-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 675d56330..45c9ed595 100644 --- a/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx +++ b/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx @@ -50,6 +50,7 @@ import { } 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"; @@ -269,8 +270,24 @@ const providersWithAuth: ReleaseProviderKind[] = [ 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() }); - const deleteSecret = clientApi.widget.secrets.deleteSecret.useMutation({ onSuccess: () => refetch() }); + 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); 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-system-stats/component.tsx b/packages/widgets/src/beszel-system-stats/component.tsx index c82196639..21b21981f 100644 --- a/packages/widgets/src/beszel-system-stats/component.tsx +++ b/packages/widgets/src/beszel-system-stats/component.tsx @@ -21,6 +21,7 @@ import { CPU_Y_AXIS_DOMAIN, useContainerNames, useDockerChartData, + useEfsChartData, useSystemChartData, } from "../beszel/_shared/chart"; import { @@ -185,11 +186,31 @@ export default function BeszelSystemStatsWidget({ timePeriod, ); + const efsPaths = useMemo(() => { + if (!systemStats) return []; + const paths = new Set(); + for (const record of systemStats) { + if (record.stats.efs) { + for (const path of Object.keys(record.stats.efs)) { + paths.add(path); + } + } + } + return [...paths]; + }, [systemStats]); + + const storageData = useEfsChartData(statsWhenShown(options.showStorage, systemStats), efsPaths, timePeriod); + const containerSeries = useMemo( () => containerNames.map((name, i) => ({ name, color: containerColors[i % containerColors.length] as string })), [containerNames], ); + const storageSeries = useMemo( + () => efsPaths.map((path, i) => ({ name: path, color: containerColors[i % containerColors.length] as string })), + [efsPaths], + ); + const cols = gridColumns[Number(width > 600)]; if (systemsError) throw systemsError; @@ -385,6 +406,21 @@ export default function BeszelSystemStatsWidget({ /> )} + {options.showStorage && storageData.length > 0 && storageSeries.length > 0 && ( + + )} + {showDocker && containerSeries.length > 0 && ( <> {options.showDockerCpu && dockerCpuData.length > 0 && ( diff --git a/packages/widgets/src/beszel-system-stats/index.ts b/packages/widgets/src/beszel-system-stats/index.ts index a184963e7..3e48a1e4f 100644 --- a/packages/widgets/src/beszel-system-stats/index.ts +++ b/packages/widgets/src/beszel-system-stats/index.ts @@ -45,6 +45,7 @@ export const { definition, componentLoader } = createWidgetDefinition("beszelSys showDockerCpu: factory.switch({ defaultValue: true }), showDockerMemory: factory.switch({ defaultValue: true }), showDockerNetwork: factory.switch({ defaultValue: true }), + showStorage: factory.switch({ defaultValue: true }), })); }, errors: { diff --git a/packages/widgets/src/beszel/_shared/chart.tsx b/packages/widgets/src/beszel/_shared/chart.tsx index 8d8a5e319..0866dc9d8 100644 --- a/packages/widgets/src/beszel/_shared/chart.tsx +++ b/packages/widgets/src/beszel/_shared/chart.tsx @@ -174,6 +174,25 @@ const defaultContainerExtractors: Record = { network: (c) => (c?.b ? c.b[0] + c.b[1] : (c?.ns ?? 0) + (c?.nr ?? 0)), }; +export const useEfsChartData = ( + systemStats: BeszelSystemStatsRecord[] | undefined, + efsPaths: string[], + timePeriod: BeszelTimePeriod = "1h", +) => + useMemo(() => { + 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 }; + const efs = record.stats.efs ?? {}; + for (const path of efsPaths) { + point[path] = efs[path]?.du ?? 0; + } + return point; + }); + return padTimeGrid(mapped, timePeriod); + }, [systemStats, efsPaths, timePeriod]); + export const useDockerChartData = ( containerStats: BeszelContainerStatsRecord[] | undefined, containerNames: string[], diff --git a/packages/widgets/src/beszel/_shared/use-live-stats.ts b/packages/widgets/src/beszel/_shared/use-live-stats.ts index b07bbbd2c..f8c351bf8 100644 --- a/packages/widgets/src/beszel/_shared/use-live-stats.ts +++ b/packages/widgets/src/beszel/_shared/use-live-stats.ts @@ -1,15 +1,64 @@ "use client"; +import { useCallback, useEffect, useRef, useState } from "react"; + import { clientApi } from "@homarr/api/client"; +import type { BeszelContainerStatsRecord, BeszelSystemStatsRecord } from "@homarr/integrations/types"; + +// 120 records at ~1 per second = 2 minutes of rolling data for charts +const MAX_BUFFER = 120; export const useLiveStats = (integrationIds: string[], systemId: string, enabled: boolean) => { - const { data, error } = clientApi.widget.beszel.getSystemStats.useQuery( - { integrationIds, systemId, timePeriod: "1m" as const, includeDocker: true }, - { enabled: enabled && systemId !== "", refetchInterval: 5_000 }, + const [systemStats, setSystemStats] = useState([]); + const [containerStats, setContainerStats] = useState([]); + const [error, setError] = useState(null); + + // 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 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( + { integrationIds, systemId }, + { + enabled: enabled && systemId !== "", + 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))); + }, + }, ); - return { - data: data ? { systemStats: data.systemStats, containerStats: data.containerStats } : null, - error: error ?? (data?.error ? new Error(data.error) : null), - }; + // Reset buffers when system changes or integration changes + const prevKeyRef = useRef(""); + const currentKey = `${integrationIds.join(",")}:${systemId}`; + useEffect(() => { + if (prevKeyRef.current !== currentKey) { + prevKeyRef.current = currentKey; + setSystemStats([]); + setContainerStats([]); + setError(null); + } + }, [currentKey]); + + const data = systemStats.length > 0 || containerStats.length > 0 ? { systemStats, containerStats } : null; + + return { data, error }; }; 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/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 8b77b1d2f..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"; @@ -65,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/index.tsx b/packages/widgets/src/index.tsx index ca8a5f62f..061fc4b2d 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"; @@ -89,10 +93,12 @@ export const widgetImports = { downloads, "mediaRequests-requestList": mediaRequestsList, "mediaRequests-requestStats": mediaRequestsStats, + mediaMissing, networkControllerSummary, networkControllerStatus, rssFeed, bookmarks, + bazarr, indexerManager, healthMonitoring, mediaTranscoding, @@ -109,6 +115,7 @@ export const widgetImports = { "immich-serverStats": immichServerStats, "immich-albumCarousel": immichAlbumCarousel, paperlessNgx, + patchmon, tracearr, speedtestTracker, uptimeKuma, @@ -120,6 +127,7 @@ export const widgetImports = { beszelSystemGrid, beszelAlerts, beszelSystemStats, + traefik, customApi, } satisfies WidgetImportRecord; 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/modals/widget-edit-modal.tsx b/packages/widgets/src/modals/widget-edit-modal.tsx index d1713298c..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"; @@ -54,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()), diff --git a/packages/widgets/src/notifications/component.tsx b/packages/widgets/src/notifications/component.tsx index 3b7cc5ba7..3b04393d0 100644 --- a/packages/widgets/src/notifications/component.tsx +++ b/packages/widgets/src/notifications/component.tsx @@ -1,7 +1,7 @@ "use client"; import { useMemo } from "react"; -import { Card, Flex, Group, ScrollArea, Stack, Text } from "@mantine/core"; +import { Avatar, Card, Flex, Group, ScrollArea, Stack, Text } from "@mantine/core"; import { IconClock } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; @@ -34,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 bf82ef226..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"; @@ -72,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; @@ -196,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 ?? "", @@ -242,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); @@ -256,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/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..b59224622 --- /dev/null +++ b/packages/widgets/src/patchmon/component.tsx @@ -0,0 +1,472 @@ +"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 { + resolveStatColor, + severityToIconColor, + severityToMantineBgColor, + severityToMantineColor, + type PatchMonStatKey, +} 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/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 d24a68c37..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"]; @@ -123,12 +124,25 @@ export default function SystemResources({ integrationIds, options }: WidgetCompo 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/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/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..91de88ffb --- /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 ( +
+ {t(`protocol.${protocol}`)} +
+ {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/ups/component.tsx b/packages/widgets/src/ups/component.tsx index 2cb95abbb..2b3d93bd9 100644 --- a/packages/widgets/src/ups/component.tsx +++ b/packages/widgets/src/ups/component.tsx @@ -10,7 +10,6 @@ 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 { NoIntegrationSelectedError } from "../errors/no-integration-selected"; const statusColors: Record = { @@ -48,7 +47,7 @@ function UpsContent({ integrationIds, options, width }: UpsContentProps) { ); 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/pnpm-lock.yaml b/pnpm-lock.yaml index a50c9f57f..574aad9dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,8 +49,8 @@ catalogs: specifier: ^2.6.0 version: 2.6.0 '@immich/sdk': - specifier: ^3.0.0 - version: 3.0.0 + specifier: ^3.0.1 + version: 3.0.1 '@jellyfin/sdk': specifier: ^0.13.0 version: 0.13.0 @@ -100,8 +100,8 @@ catalogs: specifier: ^1.61.1 version: 1.61.1 '@scalar/api-reference-react': - specifier: ^0.9.49 - version: 0.9.49 + specifier: ^0.9.51 + version: 0.9.52 '@semantic-release/changelog': specifier: ^6.0.3 version: 6.0.3 @@ -112,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 @@ -127,29 +127,29 @@ catalogs: specifier: ^3.44.0 version: 3.44.0 '@tanstack/query-async-storage-persister': - specifier: ^5.101.0 - version: 5.101.0 + 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.0 - version: 5.101.0 + 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 @@ -199,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.10.0 - version: 2.10.0 + specifier: ^2.10.2 + version: 2.10.2 '@types/adm-zip': specifier: 0.5.8 version: 0.5.8 @@ -286,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 @@ -295,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 @@ -307,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 @@ -397,8 +397,8 @@ catalogs: specifier: ^4.5.0 version: 4.5.0 lucide-react: - specifier: ^1.22.0 - version: 1.22.0 + specifier: ^1.23.0 + version: 1.23.0 mantine-datatable: specifier: ^9.3.1 version: 9.3.1 @@ -415,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 @@ -430,11 +430,11 @@ catalogs: specifier: ^5.0.5 version: 5.0.5 oxfmt: - specifier: ^0.56.0 - version: 0.56.0 + specifier: ^0.57.0 + version: 0.57.0 oxlint: - specifier: ^1.71.0 - version: 1.71.0 + specifier: ^1.72.0 + version: 1.72.0 pg: specifier: ^8.22.0 version: 8.22.0 @@ -442,11 +442,11 @@ catalogs: specifier: ^1.18.0 version: 1.18.0 posthog-js: - specifier: ^1.395.0 - version: 1.395.0 + 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 @@ -475,8 +475,8 @@ catalogs: specifier: ^0.14.1 version: 0.14.1 recharts: - specifier: ^3.9.0 - version: 3.9.0 + specifier: ^3.9.1 + version: 3.9.1 sass: specifier: ^1.101.0 version: 1.101.0 @@ -496,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 @@ -508,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.10.0 - version: 2.10.0 + 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 @@ -529,8 +529,8 @@ catalogs: specifier: ^8.23.9 version: 8.23.9 vite: - specifier: ^8.1.0 - version: 8.1.0 + specifier: ^8.1.2 + version: 8.1.3 vite-tsconfig-paths: specifier: ^6.1.1 version: 6.1.1 @@ -565,20 +565,20 @@ overrides: 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.0' + esbuild@<=0.24.2: '>=0.28.1' form-data@>=4.0.0 <4.0.4: '>=4.0.6' hono@<4.6.5: '>=4.12.27' linkifyjs@<4.3.2: '>=4.3.3' 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.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.1.0' + vite@>=5.0.0 <=5.4.18: '>=8.1.3' patchedDependencies: '@types/node-unifi': 5e6ae51e2a17a7f9729bfa30b0eb3d0842a5810ac6db47603ab4a6efa1ed84c5 @@ -606,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)) @@ -615,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.10.0(@types/node@24.13.2) + version: 2.10.2(@types/node@24.13.2) '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.3(babel-plugin-react-compiler@1.0.0)(vite@8.1.0(@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)) + 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) @@ -630,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 @@ -642,31 +642,31 @@ importers: version: 2.2.3 oxfmt: specifier: 'catalog:' - version: 0.56.0 + version: 0.57.0 oxlint: specifier: 'catalog:' - version: 1.71.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.10.0 + version: 2.10.2 typescript: specifier: 'catalog:' version: 6.0.3 vite: specifier: 'catalog:' - version: 8.1.0(@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) + 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.1.0(@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)) + 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.1.0(@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)) + 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: @@ -702,7 +702,7 @@ 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.49(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.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)) @@ -710,8 +710,8 @@ importers: 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 @@ -743,11 +743,11 @@ 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 @@ -913,37 +913,37 @@ importers: version: 1.29.0(zod@4.4.3) '@scalar/api-reference-react': specifier: 'catalog:' - version: 0.9.49(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-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.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) + 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.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) '@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.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) + 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,7 +994,7 @@ importers: version: 3.10.1 lucide-react: specifier: 'catalog:' - version: 1.22.0(react@19.2.7) + version: 1.23.0(react@19.2.7) mantine-datatable: specifier: 'catalog:' 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) @@ -1003,16 +1003,16 @@ importers: 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.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)) + 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.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) + 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.395.0 + 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.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.9(@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) + 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) @@ -1151,7 +1151,7 @@ importers: 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.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) + 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.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.9(@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) + 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.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) + 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.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) + 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.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) + 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 @@ -1688,13 +1688,13 @@ importers: 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) @@ -1923,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 @@ -1935,7 +1935,7 @@ importers: version: link:../validation '@immich/sdk': specifier: 'catalog:' - version: 3.0.0 + version: 3.0.1 '@jellyfin/sdk': specifier: 'catalog:' version: 0.13.0(axios@1.15.0) @@ -1956,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 @@ -2057,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.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) + 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 @@ -2137,10 +2137,10 @@ importers: 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.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) + 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 @@ -2264,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 @@ -2305,7 +2305,7 @@ importers: 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.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) + 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 @@ -2372,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.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) + 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 @@ -2409,10 +2409,10 @@ importers: 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.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) + 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.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) + 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 @@ -2464,7 +2464,7 @@ importers: 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.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) + 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 @@ -2583,7 +2583,7 @@ importers: version: link:../validation '@mantine/charts': specifier: 'catalog:' - 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.0(@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.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) @@ -2655,7 +2655,7 @@ importers: version: 10.4.0 lucide-react: specifier: 'catalog:' - version: 1.22.0(react@19.2.7) + version: 1.23.0(react@19.2.7) mantine-datatable: specifier: 'catalog:' 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) @@ -2664,7 +2664,7 @@ importers: 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.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) + 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 @@ -2682,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.9.0(@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 @@ -3608,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'} @@ -4239,9 +4243,6 @@ packages: '@emnapi/core@1.11.1': resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - '@emnapi/runtime@1.11.1': resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} @@ -5177,8 +5178,8 @@ packages: cpu: [x64] os: [win32] - '@immich/sdk@3.0.0': - resolution: {integrity: sha512-HUT+L5LEIRsW65GtGj1hLqv9XQzIWzEyaAnAUsVVYHAc3hs+bD8p+KV9Km6Yw8Kg5eOqt+pDcp0peHYkgwnZJQ==} + '@immich/sdk@3.0.1': + resolution: {integrity: sha512-+iY6cd5Fc0cDIQOpXCSJHJwByaNeFj5MXzFT0xqRlXLE1G2fizoRt48BQ5eVoI64swDtwevnFIlCLYgydF/qqw==} '@inquirer/ansi@1.0.2': resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} @@ -5730,57 +5731,57 @@ packages: '@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] @@ -5850,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==} @@ -5995,249 +5999,249 @@ packages: resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==} engines: {node: '>=14'} - '@oxc-project/types@0.137.0': - resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} + '@oxc-project/types@0.138.0': + resolution: {integrity: sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==} - '@oxfmt/binding-android-arm-eabi@0.56.0': - resolution: {integrity: sha512-CSCxi7ovYojgfdPOdUb9T508HKeAdDIKeRGg7x8IZwVJrWz9gVgX7MbUnFqtQAE4QvoNo07mj2JlwnOzJw4qqA==} + '@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.56.0': - resolution: {integrity: sha512-HYJFnd+PkDwf6S9ZPGzXXtjNqvRWFnnhdbWaouh4mi/SxU8wmDuzlMn3xo/wDTGnr4Q1VA7ZzOaE/D4biW0W6A==} + '@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.56.0': - resolution: {integrity: sha512-sftR/bEOr+t1gs+evwsHi/Xbq2FAPA2uU3VMr8n6ZU9PoK/IMSfnfu7+OEe/uy1+knhrFl4Wvy7Vkm3uo9mJ7g==} + '@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.56.0': - resolution: {integrity: sha512-z66SdjLqa3MUPKvTp3Mbb5nSjKSbnYxJGeB+Wx987s8T5hPcIRiBMfnJ6zcPgYtQn3x5xjvdzNVkXrSeYH6ZFg==} + '@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.56.0': - resolution: {integrity: sha512-t2tkrV1vtZyaItSQ71dTi2ZVKZEI39b/LqLT12V5KMfIeXK6N32TUC1jhOXKVQmhECq9j2ZXMQV3JeT1kh9Vmg==} + '@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.56.0': - resolution: {integrity: sha512-+gCy+Tp3RHeXQ9y/QrS76lXIpZkbziTyp6hIgjB2MssCwfMph3vG/GEfkhO34Rai1vhYIaUkvv8UT1BcDorJPw==} + '@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.56.0': - resolution: {integrity: sha512-0kKkVvQ2I+FJ2sxQyUu1zJ0yWP5kcWse/yVFnGQSFCXMwSSkfEaUGu0dW774O7nyy3jrcBGap7OSc8dZmU/CdA==} + '@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.56.0': - resolution: {integrity: sha512-npkA2siMbyWRh+wEhi1aTAx4RirukGcGNt8V4Ch86pG+xU9aurqS1MZOnKYMu03ISwat3rB6zkQx51SsB9obNw==} + '@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.56.0': - resolution: {integrity: sha512-UekqOjGkV4/MkqreCV9SPIB2jlR3/HbXrmhV1rVXJZ9wfDXMyCMriLtq3tHqLY4PkbVWNtfcm1kMojJ26KLSJw==} + '@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.56.0': - resolution: {integrity: sha512-XSzveSpeZMD5XJpew5lRFVtNnT04xd3rJxENXmk7wkZzN9oWzv2aFJyoNDhOtoz69BYaS/fg4SYl+CfEZRpB0Q==} + '@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.56.0': - resolution: {integrity: sha512-EkQ0nJa7k7HDDIVuPF7WY+k4k+bzdclLYtyIXNt7/OqVghfNiMym6YGppFBgx1XRIHW6QylxBz5OogumPjPJbQ==} + '@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.56.0': - resolution: {integrity: sha512-dyjAGW8jKRge0ik6U/dgvQG0nVpA3iBlRskQTz5qJLvQWIrySxX5jpqzPetLBNIIZ231KA82fDdi1nLTk8ENCw==} + '@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.56.0': - resolution: {integrity: sha512-60ZGH3LtfqlW8X6vcLdSFY4lvCQYINurttYBKaALnHCDVAUCYJ1LsUgS6p1XOzVlzEDx3yNUZvDF1Lvt59zoZw==} + '@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.56.0': - resolution: {integrity: sha512-u1suj1tgJHK4ZqB7buCtdbNef2n8+d0lXTPJwLHNmtyK6p+DTpsaoDvmqhQrA56fgKYv4LuRxNtL8YooebKOew==} + '@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.56.0': - resolution: {integrity: sha512-aYGLvlQHt80y+qKEtfJY/Nm27G0125Lv+qyh9SJ4Cjc6lXnXjD+ndfhqQnbV24POpMi7rNRi0jvx/0d70FRpCQ==} + '@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.56.0': - resolution: {integrity: sha512-H/re/gO+7ysVc+kywHNuzY3C33EN9sQcZhg0kp1ZwOZl7y998ZE5mhnBiuGR/nYI0pqLL5xQfrHVUOJ/cIJsCA==} + '@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.56.0': - resolution: {integrity: sha512-6qLNXfXmtAs8jXDvYMkxk6Wec5SUJoew+ZX1uOZmqaR7ks0EJFbAohuOCELDyJMWyVlxotVG8Xf8m74Bfq0O2w==} + '@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.56.0': - resolution: {integrity: sha512-UXEXuKphAe15bsob4AswNMArCw38XSmUIs3wk1s6e6MX9OWGW/IRWU95s1hZDiVg09STy1jHgyN2qkqbu1FT0w==} + '@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.56.0': - resolution: {integrity: sha512-HPyNDjky+NIOuaMvHZflR+kst3YWdUOH2JUQYkf99grqZ5mEBTQM6h9iGy501Z8Xt5xMScrwHOuVCOlqDrktRw==} + '@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.71.0': - resolution: {integrity: sha512-ImGmd1njEg4FEJH03jhRnveEegtO3czCtfptvaHivKAZQIYATbVFBrrzbaYMYv0oJioTnxZAZVSyV+oL7W8S2g==} + '@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.71.0': - resolution: {integrity: sha512-4A5BEexBrwY1YFF8Kiq/lp/wQPRG79G3BWIE1FuWaM5MvmpYSd+7ZySVcKkHdwo0UDzdQGddp6pD9mpctMqLnw==} + '@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.71.0': - resolution: {integrity: sha512-9wJA9GJulLwS2usU3CEisI/ESDO1n1z9eyTCvApMDrAkbJ1ve0mORgTMjcWWsKxkzkeZ2N/Gpra5IQE7x8tYgQ==} + '@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.71.0': - resolution: {integrity: sha512-PlLCjS06V0PeJMAJwzjrExw1sYNW9Gch3JtNlcwwZDXGlTYDuwHNN89zYH8LTXFfgkVtsYvs2nv0FqrzyuFDzg==} + '@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.71.0': - resolution: {integrity: sha512-Lhil7bWre0ncxbUoDoxfS0JzpTz17BRQKW7iwoAUY8GJ66+WwJEfYPCFJ1P0WgVZR5/O/b3Q2pENlHOjeXLOGQ==} + '@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.71.0': - resolution: {integrity: sha512-Oo9/L58PYD3RC0x05d2upAPLllHytTjHQGsnC06P6Ynn7jKkp5mdImQxXdJ3+FnBaKspNpGogzgVsi6g872LiA==} + '@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.71.0': - resolution: {integrity: sha512-mSHfyfgJrEbyIR29ejaeS50BdPk+GoNPlC1dckpDiUZbJAIel68sjSMdOt4WY0/gva+ECC7FNITQkxMJU+vSBw==} + '@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.71.0': - resolution: {integrity: sha512-n9yY4M2tiy3aij4AqtlnspzpfdpeT5JQfK2/w2d8oyp5W0FRwOb1dIeX99nORNcxGr08iD9bH8N5XFz3I2iy8w==} + '@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.71.0': - resolution: {integrity: sha512-fJZrs5sDZtTaPIOiemRQQmo82Ezy+vOGXemPc4Ok7iVVsYsFa7SlW6Z5XN819VfsqBHRm3NJ3rTdnR8+bJYJdQ==} + '@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.71.0': - resolution: {integrity: sha512-cwl7VKGERIy9p+G+AvZdfy/06q0aHXaTt/mMRReC751iuNYJgqKjB7NydXSS30nBT9vtr2tunciOtrR4fD6FUA==} + '@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.71.0': - resolution: {integrity: sha512-eZ8ieVXvzGi8jr7+ybQGPK2STw3mldfxZlgA2738iflfB/rzA69sE6m5rDRpQaxC7dpm745Enlh1Tod0QAk9Gg==} + '@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.71.0': - resolution: {integrity: sha512-puMDbQYe6+NXwfMusojoA7CXGn2b3utukmd23PQqc1E3XhVCwyZ+FueSMzDYeNgDV2dUfIVXAAKZBcFDeCL6sA==} + '@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.71.0': - resolution: {integrity: sha512-4NJLxBs1ujISCt3L/1FcywLs73PWtJuw+piD6feK2V6h6OS6P7xu9/sWt1DTRLibe6QCzmfZzmM/2HPORoV/Lg==} + '@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.71.0': - resolution: {integrity: sha512-cFDaiR8L3430qp88tfZnvFlt3KotFhR/DlbIL0nHOMMYiG/9Wy4l+6f7t8G8pTa9bd8Lt8+M0y/qjRQ/xcB74g==} + '@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.71.0': - resolution: {integrity: sha512-orfixdt76KlpNly9z0PkWBBNfwjKz+JFVLP/7wnVchlKNU9Dpt9InU/ZggeSej6fC7qwHmHNOGlhLnQXcYoGuA==} + '@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.71.0': - resolution: {integrity: sha512-9emQu2lAp6yhPB3XuI+++vR+l/o6JR1X+EpxwcumPdQXBWXEPAsquPGL7l158EqU8SebQMXTUa/S5zN98juyHw==} + '@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.71.0': - resolution: {integrity: sha512-bd5kI8spYwTm3BILDtGhi73zoup5dw8MlPQNT8YB3BD5UIsjNe3K9/4ctrzQMX4SZMoK5HgzVLkLJzacEXB7fA==} + '@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.71.0': - resolution: {integrity: sha512-W4HvOHGzVLHcrmFu+bMrJlho+/yrlX5ZNdJZqGe8MEldkQG+RHYhxxad9P4jvWAYFmIqUA5i9DQ8QsJqSU9GIw==} + '@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.71.0': - resolution: {integrity: sha512-D2kyEIPHk/G/wiZLnwTVC/sVst+T/lKldVOjAFpgTIBUAOlry72e5OiapDbDBF4LfJLkN5ypJb/8Eu6yJzkveQ==} + '@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] @@ -6400,11 +6404,11 @@ packages: '@polka/url@1.0.0-next.25': resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==} - '@posthog/core@1.38.0': - resolution: {integrity: sha512-QLrJh0hMVpEJXHNyiJR9YhvIO5tzLDedw4UHdvB+ub4fXHMtHCA8H44qgl11HWR3ajpjxtJ8hs3HyOGVF3Zc1w==} + '@posthog/core@1.39.6': + resolution: {integrity: sha512-o6ajIwN5zXoNP0D4H/QPmOyibNTUkSyOR6ya7AG5U2ywXx4awo72L2KnCoiZPQM5x/bXv6jPBdimH8M18Ax0aw==} - '@posthog/types@1.391.1': - resolution: {integrity: sha512-ASwd7Nf4pViqdYRYaNRyPYRVKWa1CcHUAUWR0XeQJLGdNnsWACBwe0sSieb/cHnKsRXjRwO/23KIY83lm/Ccpw==} + '@posthog/types@1.392.0': + resolution: {integrity: sha512-nctNujXL3FC1v99FktaTMSugSD9ZOZekEpahUSafkU2TSvW+XGKNkQZbokuJtiWvPBK208dwMJva8UfBkChqpw==} '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -6514,97 +6518,97 @@ packages: '@codemirror/state': ^6.0.0 '@codemirror/view': ^6.0.0 - '@rolldown/binding-android-arm64@1.1.3': - resolution: {integrity: sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==} + '@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.1.3': - resolution: {integrity: sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==} + '@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.1.3': - resolution: {integrity: sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==} + '@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.1.3': - resolution: {integrity: sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==} + '@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.1.3': - resolution: {integrity: sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==} + '@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.1.3': - resolution: {integrity: sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==} + '@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.1.3': - resolution: {integrity: sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==} + '@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.1.3': - resolution: {integrity: sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==} + '@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.1.3': - resolution: {integrity: sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==} + '@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.1.3': - resolution: {integrity: sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==} + '@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.1.3': - resolution: {integrity: sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==} + '@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.1.3': - resolution: {integrity: sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==} + '@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.1.3': - resolution: {integrity: sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==} + '@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.1.3': - resolution: {integrity: sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==} + '@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.1.3': - resolution: {integrity: sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==} + '@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] @@ -6689,26 +6693,30 @@ packages: '@rvf/set-get@7.0.1': resolution: {integrity: sha512-GkTSn9K1GrTYoTUqlUs36k6nJnzjQaFBTTEIqUYmzBcsGsoJM8xG7EAx2WLHWAA4QzFjcwWUSHQ3vM3Fbw50Tg==} - '@scalar/agent-chat@0.12.13': - resolution: {integrity: sha512-JPjwnOqIRb/TF+0bVNL7rg9SSjce3p0G++GF8uNvoGthixWuQxzo2pems4npZwe6Aw57icRTsZ3GBFgs9VmJvw==} + '@scalar/agent-chat@0.12.16': + resolution: {integrity: sha512-YETdit7xhWpqdu4PMDM6AVQXID6GtJ1MSS0pmdyUFpR1xG02ABFWPsZHJ8+LQvFQXTC9ZOowtjgP8G9gTKA+KQ==} engines: {node: '>=22'} - '@scalar/api-client@3.12.0': - resolution: {integrity: sha512-NBZTMuqG9Qu+C0bUtMJPQEbGBLC5skXj55A/oDSzPwvTmWKV6I9Xc4wcrqVZMMidx315uC/WWyrB9WCUMgNi3Q==} + '@scalar/api-client@3.13.1': + resolution: {integrity: sha512-+UWLBGY2dFFrbCYtEBHpjezZvSg/7eJ+CZs3oWo3Rr6JmVIJ/CKN31Ev5KAfce0w5Xmf7vbaWdExZheW+3ff7Q==} engines: {node: '>=22'} - '@scalar/api-reference-react@0.9.49': - resolution: {integrity: sha512-dtOrWZ++rFfsihNZKZ2pJ722bSkI3qp5CZKdiOY98PoJUWwPoSxT7C3NAn4NX59jQ0kuLhWDEELDpAcA779cvA==} + '@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.62.0': - resolution: {integrity: sha512-QYPqMPfy4vyVLysHeSFZsD+717ua/gHIhYYu7UjqPzJUOVh2R+/9hINKKzvoP13lQWCQTmuzs2VEl8rQOIfDjw==} + '@scalar/api-reference@1.62.3': + resolution: {integrity: sha512-0Q4NGSVK34tR/YMeQGxDD+v2NfH87/vDPxaBXxSyxfjg3lx/pmidt+YW2vcpxpoQdaPOVVwm8iuzcg0cxFjkCg==} engines: {node: '>=22'} - '@scalar/blocks@0.1.0': - resolution: {integrity: sha512-oFnIa6dAkCt0N5yw5aqCABbP7r7YsI7ERNrGtlTDdrCteDRatMJg9y03LT+9Vhf2/K3VHWrdCZMruUdfU1uPzQ==} + '@scalar/asyncapi-upgrader@0.1.2': + resolution: {integrity: sha512-h6NUhsctrhucrbO2XHWbTXp9EWt7eL5vvlsooUEw2bB014KSPd41JrBQ6t0h/dFdmhwxdbBI7Hmg9eZa/mlCXA==} + engines: {node: '>=22'} + + '@scalar/blocks@0.1.2': + resolution: {integrity: sha512-vJQFlEckV4NTn5x95Q1Ff5HeeHwAqaX/8MLOX3O63F/XFXotIAp+MS8BQG2jnlVxzQn5omRSCNug2Y5J5ETZEw==} engines: {node: '>=22'} '@scalar/code-highlight@0.4.0': @@ -6731,8 +6739,8 @@ packages: resolution: {integrity: sha512-Vw2nrUDIjhvMP6vxFtkiiFlabJ6SyTtfn1BsOxgnr1hIB+/rkngMguiDzl5em21VjyfFGIoADia+QWKM2hdcdA==} engines: {node: '>=22'} - '@scalar/oas-utils@0.19.1': - resolution: {integrity: sha512-FflVWiFDm0s4a+FqKJ0uYsVhDzbAtVyXSyUWsGe9BGPPQSXYYvVEVp9H2ir4xueaw4TZb1cDEZbteHLx7h7vqg==} + '@scalar/oas-utils@0.19.3': + resolution: {integrity: sha512-+h/vLMfGj/mpr5FYgDLFIc+X75sYH2rc2MJNSvFTmmjMeerJDVINEYs+aYOx1Mn9Z3HnoP+qQohoWTJpg2gh7A==} engines: {node: '>=22'} '@scalar/openapi-types@0.9.1': @@ -6743,16 +6751,16 @@ packages: resolution: {integrity: sha512-D5b0rGLLZgmkO9mdW2j/ND1KBlH1u3RCpr87HPxv9P9ZSr6PtM5iLqFOJq0ACiaHjY2mikCrxgDmnUEhTzRpHQ==} engines: {node: '>=22'} - '@scalar/schemas@0.7.0': - resolution: {integrity: sha512-Roj0e7S29yTbGojwdx/b8C1H5arVN4h/VSPrtQ5vb9NT2ZAbrfPQVmpQYrF845rKOWX7kSzIvyBwrKNEMIAkxA==} + '@scalar/schemas@0.7.1': + resolution: {integrity: sha512-80bxEp4ZOWxOm8kqhPi3kdJ2gipz2lZBSEHStAh3Z6NZPkFAOlZZYnDFwodhY0LwTihgvv4FVNmi64gziM0F1g==} engines: {node: '>=22'} - '@scalar/sidebar@0.9.25': - resolution: {integrity: sha512-RoMKTvCo801kj5J59Myqhc6YYtvjlXGNvP7qbj/TY2hfQ4XS5dH5S7t8JKJje+78MLC2Xg9OsEmCjv+gb8ta4w==} + '@scalar/sidebar@0.9.27': + resolution: {integrity: sha512-Y2HBOuUuRSJtB4sf9T5sqZZrsT8QNYP8kWuK8lqzsujf3bv7c4ixUFRNS568EMMjr0U7Zv9xa+LP0RHem3rt4g==} engines: {node: '>=22'} - '@scalar/snippetz@0.9.19': - resolution: {integrity: sha512-RVTZQqUxOiBhAuPywDmser1xOatVj7HCNmg+qpW8172MsMpx00lRGZ940WVAhjHgKglTkwHPaTdVX7gQeDrFUQ==} + '@scalar/snippetz@0.9.20': + resolution: {integrity: sha512-A3gSBYtoTmW3m511d02vUyl/W/eabX5y/EPAwz/u88LTzdwuDrKWbZ8iY+5dkz5e5EErhNOwQDwngmOCAA91hA==} engines: {node: '>=22'} '@scalar/themes@0.16.2': @@ -6762,8 +6770,8 @@ packages: '@scalar/typebox@0.1.3': resolution: {integrity: sha512-lU055AUccECZMIfGA0z/C1StYmboAYIPJLDFBzOO81yXBi35Pxdq+I4fWX6iUZ8qcoHneiLGk9jAUM1rA93iEg==} - '@scalar/types@0.16.0': - resolution: {integrity: sha512-26OSrvZjKWZ7F236wWmJajBGDVFJuvXFJqKPFqbt/PxlgZKXQXfJsZorASRQmdNogT576nxYalQ7oaYWEnQwfw==} + '@scalar/types@0.16.1': + resolution: {integrity: sha512-zzApf0dtEqztdY//3gmRJTgySGMpKnAVqcZltAzt95yuQPXbkSqxXDTitBLd1abeSeik+W6GFTIjffL8K+1wqQ==} engines: {node: '>=22'} '@scalar/use-codemirror@0.14.12': @@ -6782,8 +6790,8 @@ packages: resolution: {integrity: sha512-tpmmG+/xRE2Kn9RpflU3AIyZv08v10+E1ZrJCx7z6+/91zHVxy0M73kC1LT4/8PbYNt85ywyC8+n+D99JdMcGA==} engines: {node: '>=20'} - '@scalar/workspace-store@0.55.0': - resolution: {integrity: sha512-4EkaLfONW8qu2vigJopbjg9ahE/Z9Q+IInnVoUTZfxLp4rb2Z3ztx8LmQOym8DPpv62BvuMtJcNdQOnQ6tVLHA==} + '@scalar/workspace-store@0.55.2': + resolution: {integrity: sha512-/8BfJkave9vmweLdzH7w2TCaUFNcLu1vmE87id0QIi08enOwOExmPsz8YpUtaKLHM2bi/R2Tj/s9Uwp9i8Lttw==} engines: {node: '>=22'} '@schummar/icu-type-parser@1.21.5': @@ -6818,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' @@ -7172,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: @@ -7245,62 +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-async-storage-persister@5.101.0': - resolution: {integrity: sha512-8Y8pwMVPh4kqU9m+MhbDYBuub+i5T2M9P/0+Ae3KccX1BwMIp6AdFAx3zDgLoZcEVI830mbJYf0sjAgsawCWVw==} + '@tanstack/query-async-storage-persister@5.101.2': + resolution: {integrity: sha512-jf7zvVKZ5q2j/xuhbTIBUpw5xt6cw/ioOFQquxK7fRY0AmMQP8A0JVMOgYoeQLA4PKdeT7RxsuLAF0Lft9oJ8Q==} - '@tanstack/query-core@5.101.0': - resolution: {integrity: sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==} + '@tanstack/query-core@5.101.2': + resolution: {integrity: sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==} - '@tanstack/query-devtools@5.101.0': - resolution: {integrity: sha512-MVqw17k08RQtGGLEL654+dX/btbX9p/8WjkznO//zusLTMaObxi3Q+MoFwGVkC9K3tqjn8qrrNhJevXx4fJTeQ==} + '@tanstack/query-devtools@5.101.2': + resolution: {integrity: sha512-o+wHcqgN7Pp0s8v1i0UGq/ZrrEKrxdIiMQmKRdYb2w7NPtylYSJ4+wg/tIn71m9DLstwUwdEGAvROdly6HXP6w==} - '@tanstack/query-persist-client-core@5.101.0': - resolution: {integrity: sha512-LH99WepGVLwlLfuOcQcPK7f3Xg/Gf+xlMMIj9xWu/8oQ3egnDzjr+a4HvEmi6PGob5SmGXvmDKZaH5+In9dzjw==} + '@tanstack/query-persist-client-core@5.101.2': + resolution: {integrity: sha512-rgMsbvBVpSPDUsprC9FYxojdG0Q1LH6yq1i3DXz2aps4VEqv2l4Msj3YDqIifU3xkl/DrYe9YWntZAJLrM6xTg==} - '@tanstack/react-query-devtools@5.101.0': - resolution: {integrity: sha512-cpZA0+WqKXwrwMfiWZEGGF6QrIWVQFbhBtxqDF5sQsAfrFf47HIE6fiPbQU3wyAUEN2+7UNqLCQe7oG6m3f93w==} + '@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-persist-client@5.101.0': - resolution: {integrity: sha512-AUcdBgz8V6sM9axzdqkVmWjYSOETkhr6yAZSBnEFyZT2jo6vkFq3UrpRuxGs6fmhKMWv8FA+ZJGcbaKPaoAElQ==} + '@tanstack/react-query-persist-client@5.101.2': + resolution: {integrity: sha512-19UpaRtf0lvB2QAJ2MoixxtGf3osMOd508g99EsttUj4+3eLs+ulnNgYjB7AkQMHrRlcbVXqpVNqWkB4a7g8Zw==} peerDependencies: - '@tanstack/react-query': ^5.101.0 + '@tanstack/react-query': ^5.101.2 react: ^18 || ^19 - '@tanstack/react-query@5.101.0': - resolution: {integrity: sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg==} + '@tanstack/react-query@5.101.2': + resolution: {integrity: sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg==} peerDependencies: react: ^18 || ^19 @@ -7332,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==} @@ -7551,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' @@ -7576,65 +7584,65 @@ 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.10.0': - resolution: {integrity: sha512-EwvHThXzpY0KGd1/NAmuewI5D+aVa3Rl/OlxE36yfjUKb/+ySrfJrSlEFt8aD1OXwnnaHnQnPKHFndor0Zxlsg==} + '@turbo/darwin-64@2.10.2': + resolution: {integrity: sha512-wBM3ObqOWnKUDmg7QfUFDkDHPFUAJmrYlYqmEM8jMPAPA/I6wRJIbWimeQUqhOiQ8xPKhzyWM+xaiUP0wz8FEQ==} cpu: [x64] os: [darwin] - '@turbo/darwin-arm64@2.10.0': - resolution: {integrity: sha512-9d2fTyyG0lf5Wq1bwJA9qUaeecViMkLcdctWaMMmCkxZ/JqypmqOwK3W6vmejeKVgkr06gSoiX8bD+xN5Jpxcg==} + '@turbo/darwin-arm64@2.10.2': + resolution: {integrity: sha512-/Cq0joWnuMjDPfhjbFP4sv+C/7gkQ415zlaO4XUzD5EZxbtrKgXKvuuydMvogG8GeUnN1aDltW71RlmEfpjbyw==} cpu: [arm64] os: [darwin] - '@turbo/gen@2.10.0': - resolution: {integrity: sha512-QrnFiSKpKjijnQhde4VgEsg+WA8dQRc6EzO4iLy1+n7R8QZ3BCeVR7NePVOhhYcewoD8GZHnSPwrzu9cOvTdOA==} + '@turbo/gen@2.10.2': + resolution: {integrity: sha512-euo5g9gAjMaWt0sl8Xg2ThahSIuGvK/hqciXt8sthH+HrifzjKa9TQaIVtdTpFzsTKfOqBlUvd5O+aPrEnvxqQ==} hasBin: true - '@turbo/linux-64@2.10.0': - resolution: {integrity: sha512-sZBtjMuufitanjzi6UssoUpJMnnPlLMcdcJj3m3ptNsSq31Xh7MnjhwA5nWvLDTfEFg8GPcbYFXMo8vSdKRfqQ==} + '@turbo/linux-64@2.10.2': + resolution: {integrity: sha512-mMsf5IIhiKuceEXNstd25IbadjBXZ0amxzFOqliEzJX6HyeeHdBQPVSY583PWqYDyqM/FB8d5ZjkthfBSeuH3Q==} cpu: [x64] os: [linux] - '@turbo/linux-arm64@2.10.0': - resolution: {integrity: sha512-vkq/Z8R+1DQ+kifWFa810IjRy2NNBVvha3cg9sWA3nFh6nnGrHSMnnJKrzH7c/No9kq4Jb55Ru44YKsCSBgrKg==} + '@turbo/linux-arm64@2.10.2': + resolution: {integrity: sha512-Wcng1i2kaKmXutmwxT9MUoYZvdaIekXAdlGr4+0TpgbhGLw7nDuEcRBFrxb5BbRoX1d1q8SpdRxLc45TvDZIdQ==} cpu: [arm64] os: [linux] - '@turbo/windows-64@2.10.0': - resolution: {integrity: sha512-CRUEguLWxFQHptYZS7HjPhNhAFawfea07iR+xAQ5e4klgLrPCMdexBkXwSCwOxqTFknJ7RZFN3gOaADsw+Gttg==} + '@turbo/windows-64@2.10.2': + resolution: {integrity: sha512-SsNhM7Ho7EpAdwtrJKBOic9Hso23vu6Dp0gAfLOvUFjPzurr/sGQlXZEvr6z89ne4RDOypTwz5CBDrixpMKtXw==} cpu: [x64] os: [win32] - '@turbo/windows-arm64@2.10.0': - resolution: {integrity: sha512-dVHGaf9F8twzgibcBqKoADT/LLqf9++jDb+hq/LPWWaOmRpp4M+/pVOm7vy4z9D++xg8eaxWLT0+wQxFwhYu9A==} + '@turbo/windows-arm64@2.10.2': + resolution: {integrity: sha512-Gf+S7ICAdimT/n02bOuVWKvhHnct/HYjZg3oBNIz5hZ9ZyWHbQim9J3P5Qip8WpX0ksxF7eaBVziJCuLnjhqDg==} cpu: [arm64] os: [win32] @@ -8287,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: @@ -8565,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==} @@ -8662,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: @@ -9079,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==} @@ -9125,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'} @@ -10203,6 +10212,10 @@ 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 @@ -10424,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==} @@ -10823,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==} @@ -10844,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==} @@ -11181,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==} @@ -11516,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==} @@ -11542,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.22.0: - resolution: {integrity: sha512-c9o3l0PiNcgOQDW4F31BEYHudE7kgxVt3o30qMl36ZPwTxXlGB4QnLilhERvVM4uh/pl5MDyY1/gzZSYcHDtBg==} + 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 @@ -12062,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 @@ -12075,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: @@ -12339,8 +12352,8 @@ packages: outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} - oxfmt@0.56.0: - resolution: {integrity: sha512-9Dv0wV3zKiyvhjD7bRKaInKmHQ1sPx3RGOjQkGFJbbdQ16576yf8qhMSO9Q9cvHcs+1NpBsRTkuDDYFFPTJ6gw==} + 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: @@ -12352,8 +12365,8 @@ packages: vite-plus: optional: true - oxlint@1.71.0: - resolution: {integrity: sha512-U1m1X+C0vDj7DC1e13IoZULzEcPczE7UOMTs8VlZGHUEIUaSTZKo5qkPsQEfzpgnQ29Pea/w3Xntk62UCecxZw==} + oxlint@1.72.0: + resolution: {integrity: sha512-1rhdZIP/EvoI91ABIwNU5Q8+bWf8mjrS5UzIOZld4d4bXxJvtlUhlQvaoTogIGin/qdErMOrwaIJvCSIAKTLhA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -13115,12 +13128,17 @@ packages: resolution: {integrity: sha512-Ray65LYEJrMMqDtsBUBXunEVP/g4wtATvq/xz9rchUoLy/9mSkkFgUko/8DVtGxgiP3vivpFMgfb9HpCuDrBHg==} engines: {node: '>=10.15.1'} - posthog-js@1.395.0: - resolution: {integrity: sha512-5iTb00CGt2eQUUiBQysQiX89RAbCN6wK2sDNzvs9zv0alaY8mJ0ZySrUD3LQ+XyLhgM5pCpacBuUwChqiYDLDw==} + 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==} @@ -13509,8 +13527,8 @@ packages: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} - recharts@3.9.0: - resolution: {integrity: sha512-dCEcE9y20c8H2tkVeByrAXhhnBJk6/QLbxKmn+dJUptOfc5NMjwRh1jo0vZPRLD+5dMrHrP+hPEsfbGBMfnf5Q==} + 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 @@ -13725,8 +13743,8 @@ packages: robust-predicates@3.0.3: resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} - rolldown@1.1.3: - resolution: {integrity: sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==} + rolldown@1.1.4: + resolution: {integrity: sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -14184,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==} @@ -14272,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: @@ -14294,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==} @@ -14372,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==} @@ -14550,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: @@ -14585,8 +14606,8 @@ packages: resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} - turbo@2.10.0: - resolution: {integrity: sha512-o016H9PPtuH2deb3mh3Vci3Avfi9UYgM/RONQisY7HnloupP0IFSbFS3gFYJgFJP8nwBrByHWFQIDa8T2zIXPw==} + turbo@2.10.2: + resolution: {integrity: sha512-wTExrNrRjB8qzIcg+ZLm0A3GFNLDsWNwdS/RBXB0FPrBDyzk3i96Yx+TxWZC7a0k1SIreFB8ciUbxjmEqTH8IQ==} hasBin: true tweetnacl@0.14.5: @@ -14657,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: @@ -14806,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 @@ -14891,10 +14908,10 @@ packages: vite-tsconfig-paths@6.1.1: resolution: {integrity: sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==} peerDependencies: - vite: '>=8.1.0' + vite: '>=8.1.3' - vite@8.1.0: - resolution: {integrity: sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==} + vite@8.1.3: + resolution: {integrity: sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -16358,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: @@ -16506,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) @@ -17928,11 +17947,6 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/runtime@1.10.0': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/runtime@1.11.1': dependencies: tslib: 2.8.1 @@ -18274,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': @@ -18412,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: @@ -18423,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: @@ -18533,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': @@ -18545,7 +18559,7 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true - '@immich/sdk@3.0.0': + '@immich/sdk@3.0.1': dependencies: '@oazapfts/runtime': 1.2.0 @@ -18995,13 +19009,13 @@ snapshots: js-base64: 3.7.8 optional: true - '@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.0(@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.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.9.0(@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.4.1(chroma-js@3.2.0)': dependencies: @@ -19220,30 +19234,30 @@ snapshots: '@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)': @@ -19391,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': @@ -19573,120 +19589,120 @@ snapshots: '@opentelemetry/semantic-conventions@1.41.1': {} - '@oxc-project/types@0.137.0': {} + '@oxc-project/types@0.138.0': {} - '@oxfmt/binding-android-arm-eabi@0.56.0': + '@oxfmt/binding-android-arm-eabi@0.57.0': optional: true - '@oxfmt/binding-android-arm64@0.56.0': + '@oxfmt/binding-android-arm64@0.57.0': optional: true - '@oxfmt/binding-darwin-arm64@0.56.0': + '@oxfmt/binding-darwin-arm64@0.57.0': optional: true - '@oxfmt/binding-darwin-x64@0.56.0': + '@oxfmt/binding-darwin-x64@0.57.0': optional: true - '@oxfmt/binding-freebsd-x64@0.56.0': + '@oxfmt/binding-freebsd-x64@0.57.0': optional: true - '@oxfmt/binding-linux-arm-gnueabihf@0.56.0': + '@oxfmt/binding-linux-arm-gnueabihf@0.57.0': optional: true - '@oxfmt/binding-linux-arm-musleabihf@0.56.0': + '@oxfmt/binding-linux-arm-musleabihf@0.57.0': optional: true - '@oxfmt/binding-linux-arm64-gnu@0.56.0': + '@oxfmt/binding-linux-arm64-gnu@0.57.0': optional: true - '@oxfmt/binding-linux-arm64-musl@0.56.0': + '@oxfmt/binding-linux-arm64-musl@0.57.0': optional: true - '@oxfmt/binding-linux-ppc64-gnu@0.56.0': + '@oxfmt/binding-linux-ppc64-gnu@0.57.0': optional: true - '@oxfmt/binding-linux-riscv64-gnu@0.56.0': + '@oxfmt/binding-linux-riscv64-gnu@0.57.0': optional: true - '@oxfmt/binding-linux-riscv64-musl@0.56.0': + '@oxfmt/binding-linux-riscv64-musl@0.57.0': optional: true - '@oxfmt/binding-linux-s390x-gnu@0.56.0': + '@oxfmt/binding-linux-s390x-gnu@0.57.0': optional: true - '@oxfmt/binding-linux-x64-gnu@0.56.0': + '@oxfmt/binding-linux-x64-gnu@0.57.0': optional: true - '@oxfmt/binding-linux-x64-musl@0.56.0': + '@oxfmt/binding-linux-x64-musl@0.57.0': optional: true - '@oxfmt/binding-openharmony-arm64@0.56.0': + '@oxfmt/binding-openharmony-arm64@0.57.0': optional: true - '@oxfmt/binding-win32-arm64-msvc@0.56.0': + '@oxfmt/binding-win32-arm64-msvc@0.57.0': optional: true - '@oxfmt/binding-win32-ia32-msvc@0.56.0': + '@oxfmt/binding-win32-ia32-msvc@0.57.0': optional: true - '@oxfmt/binding-win32-x64-msvc@0.56.0': + '@oxfmt/binding-win32-x64-msvc@0.57.0': optional: true - '@oxlint/binding-android-arm-eabi@1.71.0': + '@oxlint/binding-android-arm-eabi@1.72.0': optional: true - '@oxlint/binding-android-arm64@1.71.0': + '@oxlint/binding-android-arm64@1.72.0': optional: true - '@oxlint/binding-darwin-arm64@1.71.0': + '@oxlint/binding-darwin-arm64@1.72.0': optional: true - '@oxlint/binding-darwin-x64@1.71.0': + '@oxlint/binding-darwin-x64@1.72.0': optional: true - '@oxlint/binding-freebsd-x64@1.71.0': + '@oxlint/binding-freebsd-x64@1.72.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.71.0': + '@oxlint/binding-linux-arm-gnueabihf@1.72.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.71.0': + '@oxlint/binding-linux-arm-musleabihf@1.72.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.71.0': + '@oxlint/binding-linux-arm64-gnu@1.72.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.71.0': + '@oxlint/binding-linux-arm64-musl@1.72.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.71.0': + '@oxlint/binding-linux-ppc64-gnu@1.72.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.71.0': + '@oxlint/binding-linux-riscv64-gnu@1.72.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.71.0': + '@oxlint/binding-linux-riscv64-musl@1.72.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.71.0': + '@oxlint/binding-linux-s390x-gnu@1.72.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.71.0': + '@oxlint/binding-linux-x64-gnu@1.72.0': optional: true - '@oxlint/binding-linux-x64-musl@1.71.0': + '@oxlint/binding-linux-x64-musl@1.72.0': optional: true - '@oxlint/binding-openharmony-arm64@1.71.0': + '@oxlint/binding-openharmony-arm64@1.72.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.71.0': + '@oxlint/binding-win32-arm64-msvc@1.72.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.71.0': + '@oxlint/binding-win32-ia32-msvc@1.72.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.71.0': + '@oxlint/binding-win32-x64-msvc@1.72.0': optional: true '@panva/hkdf@1.2.1': {} @@ -19875,11 +19891,11 @@ snapshots: '@polka/url@1.0.0-next.25': {} - '@posthog/core@1.38.0': + '@posthog/core@1.39.6': dependencies: - '@posthog/types': 1.391.1 + '@posthog/types': 1.392.0 - '@posthog/types@1.391.1': {} + '@posthog/types@1.392.0': {} '@protobufjs/aspromise@1.1.2': {} @@ -19983,53 +19999,53 @@ snapshots: '@codemirror/state': 6.6.0 '@codemirror/view': 6.43.0 - '@rolldown/binding-android-arm64@1.1.3': + '@rolldown/binding-android-arm64@1.1.4': optional: true - '@rolldown/binding-darwin-arm64@1.1.3': + '@rolldown/binding-darwin-arm64@1.1.4': optional: true - '@rolldown/binding-darwin-x64@1.1.3': + '@rolldown/binding-darwin-x64@1.1.4': optional: true - '@rolldown/binding-freebsd-x64@1.1.3': + '@rolldown/binding-freebsd-x64@1.1.4': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.1.3': + '@rolldown/binding-linux-arm-gnueabihf@1.1.4': optional: true - '@rolldown/binding-linux-arm64-gnu@1.1.3': + '@rolldown/binding-linux-arm64-gnu@1.1.4': optional: true - '@rolldown/binding-linux-arm64-musl@1.1.3': + '@rolldown/binding-linux-arm64-musl@1.1.4': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.1.3': + '@rolldown/binding-linux-ppc64-gnu@1.1.4': optional: true - '@rolldown/binding-linux-s390x-gnu@1.1.3': + '@rolldown/binding-linux-s390x-gnu@1.1.4': optional: true - '@rolldown/binding-linux-x64-gnu@1.1.3': + '@rolldown/binding-linux-x64-gnu@1.1.4': optional: true - '@rolldown/binding-linux-x64-musl@1.1.3': + '@rolldown/binding-linux-x64-musl@1.1.4': optional: true - '@rolldown/binding-openharmony-arm64@1.1.3': + '@rolldown/binding-openharmony-arm64@1.1.4': optional: true - '@rolldown/binding-wasm32-wasi@1.1.3': + '@rolldown/binding-wasm32-wasi@1.1.4': dependencies: '@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.1.3': + '@rolldown/binding-win32-arm64-msvc@1.1.4': optional: true - '@rolldown/binding-win32-x64-msvc@1.1.3': + '@rolldown/binding-win32-x64-msvc@1.1.4': optional: true '@rolldown/pluginutils@1.0.1': {} @@ -20094,21 +20110,21 @@ snapshots: '@rvf/set-get@7.0.1': {} - '@scalar/agent-chat@0.12.13(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.12.0(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3) - '@scalar/components': 0.27.4(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) + '@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.17 '@scalar/openapi-types': 0.9.1 - '@scalar/schemas': 0.7.0 + '@scalar/schemas': 0.7.1 '@scalar/themes': 0.16.2 - '@scalar/types': 0.16.0 + '@scalar/types': 0.16.1 '@scalar/use-toasts': 0.10.2(typescript@6.0.3) '@scalar/validation': 0.6.0 - '@scalar/workspace-store': 0.55.0(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 @@ -20132,25 +20148,25 @@ snapshots: - universal-cookie - zod - '@scalar/api-client@3.12.0(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/blocks': 0.1.0(tailwindcss@4.3.1)(typescript@6.0.3) - '@scalar/components': 0.27.4(tailwindcss@4.3.1)(typescript@6.0.3) + '@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/oas-utils': 0.19.1(typescript@6.0.3) + '@scalar/oas-utils': 0.19.3(typescript@6.0.3) '@scalar/openapi-types': 0.9.1 - '@scalar/sidebar': 0.9.25(tailwindcss@4.3.1)(typescript@6.0.3) - '@scalar/snippetz': 0.9.19 + '@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.16.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.55.0(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)) '@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 @@ -20180,10 +20196,10 @@ snapshots: - typescript - universal-cookie - '@scalar/api-reference-react@0.9.49(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.62.0(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3) - '@scalar/types': 0.16.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' @@ -20202,26 +20218,26 @@ snapshots: - universal-cookie - zod - '@scalar/api-reference@1.62.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.13(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3) - '@scalar/api-client': 3.12.0(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3) - '@scalar/blocks': 0.1.0(tailwindcss@4.3.1)(typescript@6.0.3) + '@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.1)(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/oas-utils': 0.19.1(typescript@6.0.3) - '@scalar/schemas': 0.7.0 - '@scalar/sidebar': 0.9.25(tailwindcss@4.3.1)(typescript@6.0.3) - '@scalar/snippetz': 0.9.19 + '@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.0 + '@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.55.0(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 @@ -20246,15 +20262,19 @@ snapshots: - universal-cookie - zod - '@scalar/blocks@0.1.0(tailwindcss@4.3.1)(typescript@6.0.3)': + '@scalar/asyncapi-upgrader@0.1.2': dependencies: - '@scalar/components': 0.27.4(tailwindcss@4.3.1)(typescript@6.0.3) + '@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.19 + '@scalar/snippetz': 0.9.20 '@scalar/themes': 0.16.2 - '@scalar/types': 0.16.0 - '@scalar/workspace-store': 0.55.0(typescript@6.0.3) + '@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) @@ -20284,11 +20304,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@scalar/components@0.27.4(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.4.0 '@scalar/helpers': 0.9.0 @@ -20323,12 +20343,12 @@ snapshots: pathe: 2.0.3 yaml: 2.9.0 - '@scalar/oas-utils@0.19.1(typescript@6.0.3)': + '@scalar/oas-utils@0.19.3(typescript@6.0.3)': dependencies: '@scalar/helpers': 0.9.0 '@scalar/themes': 0.16.2 - '@scalar/types': 0.16.0 - '@scalar/workspace-store': 0.55.0(typescript@6.0.3) + '@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 @@ -20341,19 +20361,19 @@ snapshots: dependencies: '@scalar/openapi-types': 0.9.1 - '@scalar/schemas@0.7.0': + '@scalar/schemas@0.7.1': dependencies: '@scalar/helpers': 0.9.0 '@scalar/validation': 0.6.0 - '@scalar/sidebar@0.9.25(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.4(tailwindcss@4.3.1)(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/themes': 0.16.2 '@scalar/use-hooks': 0.4.7(typescript@6.0.3) - '@scalar/workspace-store': 0.55.0(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' @@ -20361,10 +20381,10 @@ snapshots: - tailwindcss - typescript - '@scalar/snippetz@0.9.19': + '@scalar/snippetz@0.9.20': dependencies: '@scalar/helpers': 0.9.0 - '@scalar/types': 0.16.0 + '@scalar/types': 0.16.1 js-base64: 3.7.8 stringify-object: 6.0.0 @@ -20374,7 +20394,7 @@ snapshots: '@scalar/typebox@0.1.3': {} - '@scalar/types@0.16.0': + '@scalar/types@0.16.1': dependencies: '@scalar/helpers': 0.9.0 nanoid: 5.1.16 @@ -20421,15 +20441,16 @@ snapshots: '@scalar/validation@0.6.0': {} - '@scalar/workspace-store@0.55.0(typescript@6.0.3)': + '@scalar/workspace-store@0.55.2(typescript@6.0.3)': dependencies: + '@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.7.0 - '@scalar/snippetz': 0.9.19 + '@scalar/schemas': 0.7.1 + '@scalar/snippetz': 0.9.20 '@scalar/typebox': 0.1.3 - '@scalar/types': 0.16.0 + '@scalar/types': 0.16.1 '@scalar/validation': 0.6.0 js-base64: 3.7.8 type-fest: 5.7.0 @@ -20482,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) @@ -20500,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 @@ -20825,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 @@ -20833,105 +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-async-storage-persister@5.101.0': + '@tanstack/query-async-storage-persister@5.101.2': dependencies: - '@tanstack/query-core': 5.101.0 - '@tanstack/query-persist-client-core': 5.101.0 + '@tanstack/query-core': 5.101.2 + '@tanstack/query-persist-client-core': 5.101.2 - '@tanstack/query-core@5.101.0': {} + '@tanstack/query-core@5.101.2': {} - '@tanstack/query-devtools@5.101.0': {} + '@tanstack/query-devtools@5.101.2': {} - '@tanstack/query-persist-client-core@5.101.0': + '@tanstack/query-persist-client-core@5.101.2': dependencies: - '@tanstack/query-core': 5.101.0 + '@tanstack/query-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/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.0 - '@tanstack/react-query': 5.101.0(react@19.2.7) + '@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.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-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.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) + '@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@5.101.0(@tanstack/react-query@5.101.0(react@19.2.7))(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-persist-client-core': 5.101.0 - '@tanstack/react-query': 5.101.0(react@19.2.7) + '@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.0(react@19.2.7)': + '@tanstack/react-query@5.101.2(react@19.2.7)': dependencies: - '@tanstack/query-core': 5.101.0 + '@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)': @@ -20957,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 @@ -21199,68 +21220,68 @@ 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.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/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.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) + '@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.10.0': + '@turbo/darwin-64@2.10.2': optional: true - '@turbo/darwin-arm64@2.10.0': + '@turbo/darwin-arm64@2.10.2': optional: true - '@turbo/gen@2.10.0(@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.28.1 transitivePeerDependencies: - '@types/node' - '@turbo/linux-64@2.10.0': + '@turbo/linux-64@2.10.2': optional: true - '@turbo/linux-arm64@2.10.0': + '@turbo/linux-arm64@2.10.2': optional: true - '@turbo/windows-64@2.10.0': + '@turbo/windows-64@2.10.2': optional: true - '@turbo/windows-arm64@2.10.0': + '@turbo/windows-arm64@2.10.2': optional: true '@tybys/wasm-util@0.10.3': @@ -21724,10 +21745,10 @@ snapshots: global: 4.4.0 is-function: 1.0.2 - '@vitejs/plugin-react@6.0.3(babel-plugin-react-compiler@1.0.0)(vite@8.1.0(@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))': + '@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.1.0(@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: 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 @@ -21743,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.1.0(@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: 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: @@ -21754,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.1.0(@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/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.1.0(@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: 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: @@ -21790,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.1.0(@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: 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: @@ -22016,7 +22037,7 @@ snapshots: address@1.2.2: {} - adm-zip@0.5.17: {} + adm-zip@0.5.18: {} aes-decrypter@4.0.2: dependencies: @@ -22323,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: {} @@ -22344,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.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)): + 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) @@ -22364,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.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: 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.1.0(@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: 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' @@ -22387,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 @@ -22861,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: @@ -22895,6 +22914,8 @@ snapshots: cookie@1.1.1: {} + cookie@2.0.1: {} + cookies@0.9.1: dependencies: depd: 2.0.0 @@ -23585,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: @@ -24106,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 @@ -24337,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: @@ -24840,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: {} @@ -24932,7 +24961,7 @@ snapshots: dependencies: postcss: 8.5.16 - icu-minify@4.13.0: + icu-minify@4.13.1: dependencies: '@formatjs/icu-messageformat-parser': 3.5.0 @@ -24948,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: @@ -25281,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 @@ -25544,7 +25573,7 @@ snapshots: lru.min@1.1.4: {} - lucide-react@1.22.0(react@19.2.7): + lucide-react@1.23.0(react@19.2.7): dependencies: react: 19.2.7 @@ -25613,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.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.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.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: 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.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.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.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: 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: @@ -26357,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.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-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.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: 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.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): + 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.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.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.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@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 @@ -26393,14 +26422,14 @@ 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.1 babel-plugin-react-compiler: 1.0.0 @@ -26586,51 +26615,51 @@ snapshots: outvariant@1.4.3: optional: true - oxfmt@0.56.0: + oxfmt@0.57.0: dependencies: tinypool: 2.1.0 optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.56.0 - '@oxfmt/binding-android-arm64': 0.56.0 - '@oxfmt/binding-darwin-arm64': 0.56.0 - '@oxfmt/binding-darwin-x64': 0.56.0 - '@oxfmt/binding-freebsd-x64': 0.56.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.56.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.56.0 - '@oxfmt/binding-linux-arm64-gnu': 0.56.0 - '@oxfmt/binding-linux-arm64-musl': 0.56.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.56.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.56.0 - '@oxfmt/binding-linux-riscv64-musl': 0.56.0 - '@oxfmt/binding-linux-s390x-gnu': 0.56.0 - '@oxfmt/binding-linux-x64-gnu': 0.56.0 - '@oxfmt/binding-linux-x64-musl': 0.56.0 - '@oxfmt/binding-openharmony-arm64': 0.56.0 - '@oxfmt/binding-win32-arm64-msvc': 0.56.0 - '@oxfmt/binding-win32-ia32-msvc': 0.56.0 - '@oxfmt/binding-win32-x64-msvc': 0.56.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.71.0: + oxlint@1.72.0: optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.71.0 - '@oxlint/binding-android-arm64': 1.71.0 - '@oxlint/binding-darwin-arm64': 1.71.0 - '@oxlint/binding-darwin-x64': 1.71.0 - '@oxlint/binding-freebsd-x64': 1.71.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.71.0 - '@oxlint/binding-linux-arm-musleabihf': 1.71.0 - '@oxlint/binding-linux-arm64-gnu': 1.71.0 - '@oxlint/binding-linux-arm64-musl': 1.71.0 - '@oxlint/binding-linux-ppc64-gnu': 1.71.0 - '@oxlint/binding-linux-riscv64-gnu': 1.71.0 - '@oxlint/binding-linux-riscv64-musl': 1.71.0 - '@oxlint/binding-linux-s390x-gnu': 1.71.0 - '@oxlint/binding-linux-x64-gnu': 1.71.0 - '@oxlint/binding-linux-x64-musl': 1.71.0 - '@oxlint/binding-openharmony-arm64': 1.71.0 - '@oxlint/binding-win32-arm64-msvc': 1.71.0 - '@oxlint/binding-win32-ia32-msvc': 1.71.0 - '@oxlint/binding-win32-x64-msvc': 1.71.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: {} @@ -27059,10 +27088,10 @@ snapshots: 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.16): dependencies: @@ -27132,12 +27161,12 @@ snapshots: 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.16): @@ -27161,9 +27190,9 @@ snapshots: 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.16): @@ -27317,11 +27346,11 @@ snapshots: 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.16): dependencies: @@ -27363,9 +27392,9 @@ 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.16): dependencies: @@ -27419,22 +27448,22 @@ snapshots: posthog-docusaurus@2.0.5: {} - posthog-js@1.395.0: + posthog-js@1.396.5: dependencies: - '@posthog/core': 1.38.0 - '@posthog/types': 1.391.1 + '@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: @@ -27607,7 +27636,7 @@ snapshots: proxmox-api@1.1.1: dependencies: - undici: 7.27.1 + undici: 7.28.0 proxy-addr@2.0.7: dependencies: @@ -27932,14 +27961,14 @@ snapshots: real-require@0.2.0: {} - recharts@3.9.0(@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 @@ -28228,26 +28257,26 @@ snapshots: robust-predicates@3.0.3: {} - rolldown@1.1.3: + rolldown@1.1.4: dependencies: - '@oxc-project/types': 0.137.0 + '@oxc-project/types': 0.138.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.1.3 - '@rolldown/binding-darwin-arm64': 1.1.3 - '@rolldown/binding-darwin-x64': 1.1.3 - '@rolldown/binding-freebsd-x64': 1.1.3 - '@rolldown/binding-linux-arm-gnueabihf': 1.1.3 - '@rolldown/binding-linux-arm64-gnu': 1.1.3 - '@rolldown/binding-linux-arm64-musl': 1.1.3 - '@rolldown/binding-linux-ppc64-gnu': 1.1.3 - '@rolldown/binding-linux-s390x-gnu': 1.1.3 - '@rolldown/binding-linux-x64-gnu': 1.1.3 - '@rolldown/binding-linux-x64-musl': 1.1.3 - '@rolldown/binding-openharmony-arm64': 1.1.3 - '@rolldown/binding-wasm32-wasi': 1.1.3 - '@rolldown/binding-win32-arm64-msvc': 1.1.3 - '@rolldown/binding-win32-x64-msvc': 1.1.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: {} @@ -28352,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 @@ -28828,6 +28857,8 @@ snapshots: strip-json-comments@3.1.1: {} + strnum@2.3.0: {} + strnum@2.4.1: dependencies: anynum: 1.0.1 @@ -28857,14 +28888,9 @@ snapshots: stylis@4.4.0: {} - sugarss@5.0.0(postcss@8.5.15): - dependencies: - postcss: 8.5.15 - sugarss@5.0.0(postcss@8.5.16): dependencies: postcss: 8.5.16 - optional: true super-regex@1.0.0: dependencies: @@ -28919,7 +28945,7 @@ snapshots: '@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.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: @@ -28933,7 +28959,7 @@ snapshots: tailwind-merge@3.5.0: {} - tailwindcss@4.3.1: {} + tailwindcss@4.3.2: {} tapable@2.3.3: {} @@ -29049,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 @@ -29059,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 @@ -29178,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.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.9(@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): + 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.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.9(@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)) + '@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.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.9(@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-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.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.9(@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)) + '@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 @@ -29223,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: @@ -29261,14 +29286,14 @@ snapshots: tunnel@0.0.6: {} - turbo@2.10.0: + turbo@2.10.2: optionalDependencies: - '@turbo/darwin-64': 2.10.0 - '@turbo/darwin-arm64': 2.10.0 - '@turbo/linux-64': 2.10.0 - '@turbo/linux-arm64': 2.10.0 - '@turbo/windows-64': 2.10.0 - '@turbo/windows-arm64': 2.10.0 + '@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: {} @@ -29320,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: @@ -29478,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 @@ -29582,22 +29605,22 @@ snapshots: dependencies: global: 4.4.0 - vite-tsconfig-paths@6.1.1(typescript@6.0.3)(vite@8.1.0(@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@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.1.0(@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: 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.1.0(@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@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.1.3 + postcss: 8.5.16 + rolldown: 1.1.4 tinyglobby: 0.2.17 optionalDependencies: '@types/node': 24.13.2 @@ -29610,10 +29633,10 @@ snapshots: 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.0(@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@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.1.0(@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/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 @@ -29630,7 +29653,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.1.0(@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: 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 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index fa7fa7b52..c33da635f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -14,11 +14,11 @@ 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.6 + "@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": ^3.0.0 + "@immich/sdk": ^3.0.1 "@jellyfin/sdk": ^0.13.0 "@kubernetes/client-node": ^1.4.0 "@modelcontextprotocol/sdk": 1.29.0 @@ -33,26 +33,26 @@ catalog: "@mantine/notifications": ^9.4.1 "@mantine/spotlight": ^9.4.1 "@mantine/tiptap": ^9.4.1 - "@next/eslint-plugin-next": 16.2.9 + "@next/eslint-plugin-next": 16.2.10 "@octokit/auth-app": ^8.2.0 "@paralleldrive/cuid2": ^3.3.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-async-storage-persister": ^5.101.0 - "@tanstack/react-query": ^5.101.0 - "@tanstack/react-query-devtools": ^5.101.0 - "@tanstack/react-query-next-experimental": ^5.101.0 - "@tanstack/react-query-persist-client": ^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 @@ -69,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.10.0 + "@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 @@ -89,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.49 + "@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.395.0 - posthog-node: ^4.18.0 + 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 @@ -139,20 +139,20 @@ catalog: jsonpath-plus: ^10.4.0 ldapts: 8.1.8 lodash.clonedeep: ^4.5.0 - lucide-react: ^1.22.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.56.0 - oxlint: ^1.71.0 + oxfmt: ^0.57.0 + oxlint: ^1.72.0 pg: ^8.22.0 postcss-preset-mantine: ^1.18.0 prismjs: ^1.30.0 @@ -164,22 +164,22 @@ catalog: react-markdown: ^10.1.0 react-quill-new: ^3.8.3 react-simple-code-editor: ^0.14.1 - recharts: ^3.9.0 + 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.10.0 + turbo: ^2.10.2 typescript: ^6.0.3 - vite: ^8.1.0 - 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 @@ -191,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 @@ -205,29 +206,34 @@ minimumReleaseAgeExclude: - 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.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.0" + "esbuild@<=0.24.2": ">=0.28.1" "form-data@>=4.0.0 <4.0.4": ">=4.0.6" "hono@<4.6.5": ">=4.12.27" "linkifyjs@<4.3.2": ">=4.3.3" "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.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.1.0" + "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 3613cc905..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":"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":"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":"Bergasha","avatar_url":"https://avatars.githubusercontent.com/u/25044200?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":"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 +[{"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