From ddbc27505845449f67a2f48e97731271019792d5 Mon Sep 17 00:00:00 2001 From: "homarr-update-contributors[bot]" <210161987+homarr-update-contributors[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 19:20:47 +0000 Subject: [PATCH 01/29] chore: update contributors (#6911) * chore: update contributors * style: auto-fix lint and format --------- Co-authored-by: Homarr Update Contributors <210161987+homarr-update-contributors[bot]@users.noreply.github.com> Co-authored-by: Thomas --- .../src/docs/homarr-docs-sitemap.ts | 310 +++++++++--------- static-data/opencollective-contributors.json | 2 +- static-data/translators.json | 2 +- 3 files changed, 159 insertions(+), 155 deletions(-) diff --git a/packages/definitions/src/docs/homarr-docs-sitemap.ts b/packages/definitions/src/docs/homarr-docs-sitemap.ts index d1949994d..31464816e 100644 --- a/packages/definitions/src/docs/homarr-docs-sitemap.ts +++ b/packages/definitions/src/docs/homarr-docs-sitemap.ts @@ -1,150 +1,46 @@ // This file is auto-generated by the codegen script // it uses the sitemap.xml to generate the HomarrDocumentationPath type export type HomarrDocumentationPath = - | "/about-us" - | "/api-reference" - | "/blog" - | "/blog/2023/01/11/version0.11" - | "/blog/2023/04/16/version0.12-more-widgets" - | "/blog/2023/11/10/authentication" - | "/blog/2023/12/22/updated-documentation" - | "/blog/2024/09/23/version-1.0" - | "/blog/2024/12/17/open-beta-1.0" - | "/blog/2025/01/19/migration-guide-1.0" - | "/blog/2025/08/02/using-argus" - | "/blog/archive" - | "/blog/authors" - | "/blog/authors/ajnart" - | "/blog/authors/manuel-rw" - | "/blog/authors/meierschlumpf" - | "/blog/authors/tagashi" - | "/blog/authors/walkx" - | "/blog/documentation-migration" - | "/blog/tags" - | "/blog/tags/authentication" - | "/blog/tags/breaking-changes" - | "/blog/tags/contributions" - | "/blog/tags/design" - | "/blog/tags/dnd" - | "/blog/tags/docs" - | "/blog/tags/documentation" - | "/blog/tags/gridstack" - | "/blog/tags/homarr" - | "/blog/tags/migration" - | "/blog/tags/notepad" - | "/blog/tags/security" - | "/blog/tags/translations" - | "/blog/tags/update" - | "/blog/tags/version" - | "/blog/translations" - | "/carbon-test" - | "/search" - | "/docs/tags" - | "/docs/tags/active-directory" - | "/docs/tags/administration" - | "/docs/tags/advanced" - | "/docs/tags/ai" - | "/docs/tags/analytics" - | "/docs/tags/api" - | "/docs/tags/apps" - | "/docs/tags/automation" - | "/docs/tags/background" - | "/docs/tags/backup" - | "/docs/tags/boards" - | "/docs/tags/bookmarks" - | "/docs/tags/caddy" - | "/docs/tags/certificates" - | "/docs/tags/code" - | "/docs/tags/community" - | "/docs/tags/configuration" - | "/docs/tags/connections" - | "/docs/tags/custom-widgets" - | "/docs/tags/customization" - | "/docs/tags/data" - | "/docs/tags/data-sources" - | "/docs/tags/database" - | "/docs/tags/developer" - | "/docs/tags/development" - | "/docs/tags/docker" - | "/docs/tags/donation" - | "/docs/tags/edit-mode" - | "/docs/tags/env" - | "/docs/tags/environment-variables" - | "/docs/tags/getting-started" - | "/docs/tags/google" - | "/docs/tags/groups" - | "/docs/tags/help" - | "/docs/tags/icon-picker" - | "/docs/tags/icon-repositories" - | "/docs/tags/icons" - | "/docs/tags/installation" - | "/docs/tags/integration" - | "/docs/tags/integrations" - | "/docs/tags/interface" - | "/docs/tags/jobs" - | "/docs/tags/jsx" - | "/docs/tags/layout" - | "/docs/tags/ldap" - | "/docs/tags/management" - | "/docs/tags/mcp" - | "/docs/tags/media" - | "/docs/tags/oidc" - | "/docs/tags/open-collective" - | "/docs/tags/permissions" - | "/docs/tags/pgid" - | "/docs/tags/ping" - | "/docs/tags/programming" - | "/docs/tags/proxy" - | "/docs/tags/puid" - | "/docs/tags/redis" - | "/docs/tags/responsive" - | "/docs/tags/roles" - | "/docs/tags/search" - | "/docs/tags/search-engines" - | "/docs/tags/secrets" - | "/docs/tags/security" - | "/docs/tags/self-signed" - | "/docs/tags/seo" - | "/docs/tags/server" - | "/docs/tags/settings" - | "/docs/tags/sq-lite" - | "/docs/tags/sso" - | "/docs/tags/tasks" - | "/docs/tags/technical-documentation" - | "/docs/tags/traefik" - | "/docs/tags/translations" - | "/docs/tags/unraid" - | "/docs/tags/uploads" - | "/docs/tags/users" - | "/docs/tags/variables" - | "/docs/tags/widgets" - | "/docs/advanced/command-line" - | "/docs/advanced/command-line/development" - | "/docs/advanced/command-line/fix-usernames" - | "/docs/advanced/command-line/integrations" - | "/docs/advanced/command-line/password-recovery" - | "/docs/advanced/command-line/users" - | "/docs/advanced/development/getting-started" - | "/docs/advanced/development/kubernetes" - | "/docs/advanced/environment-variables" - | "/docs/advanced/icons" - | "/docs/advanced/keyboard-shortcuts" - | "/docs/advanced/proxy" - | "/docs/advanced/running-as-different-user" - | "/docs/advanced/single-sign-on" - | "/docs/advanced/styling" + | "" + | "/docs" + | "/docs/advanced" + | "/docs/advanced/mysql-to-sqlite" + | "/docs/getting-started/after-the-installation" + | "/docs/getting-started/glossary" + | "/docs/getting-started" | "/docs/community/donate" | "/docs/community/faq" | "/docs/community/get-in-touch" + | "/docs/community" | "/docs/community/license" | "/docs/community/translations" - | "/docs/getting-started" - | "/docs/getting-started/after-the-installation" - | "/docs/getting-started/glossary" + | "/docs/management/assistant" + | "/docs/management/backup" + | "/docs/management" + | "/docs/management/mcp" + | "/docs/integrations" + | "/docs/workshop/homarr-provider" + | "/docs/workshop" + | "/docs/widgets" + | "/docs/advanced/command-line" + | "/docs/advanced/environment-variables" + | "/docs/advanced/development/getting-started" + | "/docs/advanced/development/kubernetes" + | "/docs/advanced/development/widgets-and-integrations" + | "/docs/advanced/development/workshop-operator" + | "/docs/advanced/development/workshop" + | "/docs/advanced/icons" + | "/docs/advanced/keyboard-shortcuts" + | "/docs/advanced/running-as-different-user" + | "/docs/advanced/proxy" + | "/docs/advanced/single-sign-on" + | "/docs/advanced/styling" | "/docs/getting-started/installation/docker" | "/docs/getting-started/installation/easy-panel" | "/docs/getting-started/installation/helm" | "/docs/getting-started/installation/home-assistant" + | "/docs/getting-started/installation/hostinger" + | "/docs/getting-started/installation" | "/docs/getting-started/installation/pika-pods" | "/docs/getting-started/installation/portainer" | "/docs/getting-started/installation/proxmox" @@ -155,29 +51,68 @@ export type HomarrDocumentationPath = | "/docs/getting-started/installation/source" | "/docs/getting-started/installation/synology" | "/docs/getting-started/installation/unraid" + | "/docs/management/api" + | "/docs/management/apps" + | "/docs/management/boards" + | "/docs/management/certificates" + | "/docs/management/custom-widgets/agent-authoring" + | "/docs/management/custom-widgets/authoring" + | "/docs/management/custom-widgets/component-reference" + | "/docs/management/custom-widgets/custom-jsx" + | "/docs/management/custom-widgets" + | "/docs/management/custom-widgets/requests-and-security" + | "/docs/management/custom-widgets/troubleshooting" + | "/docs/management/logs" + | "/docs/management/media" + | "/docs/management/integrations" + | "/docs/management/search-engines" + | "/docs/management/settings" + | "/docs/management/tasks" + | "/docs/management/users" | "/docs/integrations/adguard-home" | "/docs/integrations/anchor" | "/docs/integrations/archiveteam-warrior" | "/docs/integrations/aria2" | "/docs/integrations/audiobookshelf" + | "/docs/integrations/autobrr" | "/docs/integrations/bazarr" | "/docs/integrations/beszel" + | "/docs/integrations/bindery" + | "/docs/integrations/caddy" + | "/docs/integrations/changedetection" | "/docs/integrations/coolify" | "/docs/integrations/dash-dot" | "/docs/integrations/deluge" | "/docs/integrations/docker" + | "/docs/integrations/docker-labels" | "/docs/integrations/emby" + | "/docs/integrations/fileflows" + | "/docs/integrations/frigate" + | "/docs/integrations/gatus" | "/docs/integrations/glances" | "/docs/integrations/gluetun" | "/docs/integrations/gotify" + | "/docs/integrations/healthchecks" | "/docs/integrations/home-assistant" + | "/docs/integrations/homebox" | "/docs/integrations/ical" | "/docs/integrations/immich" + | "/docs/integrations/jackett" | "/docs/integrations/jellyfin" | "/docs/integrations/jellyseerr" + | "/docs/integrations/jellystat" + | "/docs/integrations/karakeep" + | "/docs/integrations/komga" | "/docs/integrations/kubernetes" | "/docs/integrations/lidarr" + | "/docs/integrations/linkwarden" + | "/docs/integrations/llama-cpp" + | "/docs/integrations/maintainerr" + | "/docs/integrations/mealie" + | "/docs/integrations/miniflux" | "/docs/integrations/navidrome" + | "/docs/integrations/netalertx" + | "/docs/integrations/netdata" | "/docs/integrations/nextcloud" | "/docs/integrations/ntfy" | "/docs/integrations/nzbget" @@ -188,44 +123,48 @@ export type HomarrDocumentationPath = | "/docs/integrations/patchmon" | "/docs/integrations/peanut" | "/docs/integrations/pi-hole" + | "/docs/integrations/plantit" | "/docs/integrations/plex" + | "/docs/integrations/prometheus" | "/docs/integrations/prowlarr" | "/docs/integrations/proxmox" | "/docs/integrations/q-bittorent" | "/docs/integrations/radarr" | "/docs/integrations/readarr" + | "/docs/integrations/romm" | "/docs/integrations/sabnzbd" + | "/docs/integrations/scrutiny" | "/docs/integrations/seerr" | "/docs/integrations/slskd" | "/docs/integrations/sonarr" | "/docs/integrations/speedtest-tracker" + | "/docs/integrations/spoolman" + | "/docs/integrations/stash" + | "/docs/integrations/syncthing-relay" | "/docs/integrations/synology" + | "/docs/integrations/tandoor" | "/docs/integrations/tdarr" | "/docs/integrations/technitium-dns" | "/docs/integrations/tracearr" | "/docs/integrations/traefik" | "/docs/integrations/transmission" + | "/docs/integrations/trilium" | "/docs/integrations/truenas" + | "/docs/integrations/tubearchivist" | "/docs/integrations/umami" | "/docs/integrations/unifi-controller" + | "/docs/integrations/unmanic" | "/docs/integrations/unraid" | "/docs/integrations/uptime-kuma" + | "/docs/integrations/wazuh" | "/docs/integrations/whats-up-docker" - | "/docs/management/api" - | "/docs/management/apps" - | "/docs/management/backup" - | "/docs/management/boards" - | "/docs/management/certificates" - | "/docs/management/custom-widgets" - | "/docs/management/integrations" - | "/docs/management/mcp" - | "/docs/management/media" - | "/docs/management/search-engines" - | "/docs/management/settings" - | "/docs/management/tasks" - | "/docs/management/users" + | "/docs/integrations/xteve" + | "/docs/integrations/your-spotify" + | "/docs/widgets/air-quality" | "/docs/widgets/anchor-note" | "/docs/widgets/app" + | "/docs/widgets/archive-team-warrior" + | "/docs/widgets/assistant" | "/docs/widgets/audio-stats" | "/docs/widgets/bazarr" | "/docs/widgets/beszel-alerts" @@ -236,6 +175,7 @@ export type HomarrDocumentationPath = | "/docs/widgets/calendar" | "/docs/widgets/clock" | "/docs/widgets/coolify" + | "/docs/widgets/countdown" | "/docs/widgets/custom-api" | "/docs/widgets/dns-hole-controls" | "/docs/widgets/dns-hole-summary" @@ -244,9 +184,7 @@ export type HomarrDocumentationPath = | "/docs/widgets/firewall" | "/docs/widgets/health-monitoring" | "/docs/widgets/iframe" - | "/docs/widgets/immich-album-carousel" - | "/docs/widgets/immich-server-stats" - | "/docs/widgets/indexer-manager" + | "/docs/widgets/llama-cpp" | "/docs/widgets/media-missing" | "/docs/widgets/media-releases" | "/docs/widgets/media-request-list" @@ -257,6 +195,9 @@ export type HomarrDocumentationPath = | "/docs/widgets/network-controller-status" | "/docs/widgets/network-controller-summary" | "/docs/widgets/notebook" + | "/docs/widgets/immich-server-stats" + | "/docs/widgets/immich-album-carousel" + | "/docs/widgets/indexer-manager" | "/docs/widgets/notifications" | "/docs/widgets/paperless-ngx" | "/docs/widgets/patchmon" @@ -265,9 +206,10 @@ export type HomarrDocumentationPath = | "/docs/widgets/smart-home-entity-state" | "/docs/widgets/smart-home-execute-automation" | "/docs/widgets/speedtest-tracker" + | "/docs/widgets/stats" | "/docs/widgets/stock-price" | "/docs/widgets/system-disks" - | "/docs/widgets/system-resources" + | "/docs/widgets/timer" | "/docs/widgets/timetable" | "/docs/widgets/tracearr" | "/docs/widgets/traefik" @@ -278,7 +220,69 @@ export type HomarrDocumentationPath = | "/docs/widgets/vpn" | "/docs/widgets/weather" | "/docs/widgets/whats-up-docker" - | "" + | "/docs/advanced/command-line/development" + | "/docs/advanced/command-line/fix-usernames" + | "/docs/advanced/command-line/integrations" + | "/docs/advanced/command-line/password-recovery" + | "/docs/advanced/command-line/users" + | "/docs/widgets/system-resources" + | "/api-reference/apps/appRouter-getPaginated" + | "/api-reference/apps/appRouter-all" + | "/api-reference/apps/appRouter-create" + | "/api-reference/apps/appRouter-search" + | "/api-reference/apps/appRouter-selectable" + | "/api-reference/apps/appRouter-byId" + | "/api-reference/apps/appRouter-update" + | "/api-reference/apps/appRouter-delete" + | "/api-reference/boards/boardRouter-getAllBoards" + | "/api-reference/boards/boardRouter-createBoard" + | "/api-reference/boards/boardRouter-duplicateBoard" + | "/api-reference/boards/boardRouter-renameBoard" + | "/api-reference/boards/boardRouter-changeBoardVisibility" + | "/api-reference/boards/boardRouter-deleteBoard" + | "/api-reference/boards/boardRouter-setHomeBoard" + | "/api-reference/boards/boardRouter-setMobileHomeBoard" + | "/api-reference/boards/boardRouter-savePartialBoardSettings" + | "/api-reference/boards/boardRouter-addItem" + | "/api-reference/boards/boardRouter-updateItemLayout" + | "/api-reference/info/infoRouter-getInfo" + | "/api-reference/invites/inviteRouter-getAll" + | "/api-reference/invites/inviteRouter-createInvite" + | "/api-reference/invites/inviteRouter-deleteInvite" + | "/api-reference/settings/serverSettingsRouter-getBoardSettings" + | "/api-reference/settings/serverSettingsRouter-updateBoardSettings" + | "/api-reference/users/userRouter-getAll" + | "/api-reference/users/userRouter-create" + | "/api-reference/users/userRouter-setProfileImage" + | "/api-reference/users/userRouter-selectable" + | "/api-reference/users/userRouter-search" + | "/api-reference/users/userRouter-getById" + | "/api-reference/users/userRouter-delete" + | "/api-reference/users/userRouter-editProfile" + | "/api-reference/users/userRouter-changePassword" + | "/api-reference/users/userRouter-changeHomeBoards" + | "/api-reference/users/userRouter-changeDefaultSearchEngine" + | "/api-reference/users/userRouter-changeSearchPreferences" + | "/api-reference/users/userRouter-changeColorScheme" + | "/api-reference/users/userRouter-changeEnableRightClickOnWidgets" + | "/api-reference/users/userRouter-changeHeaderPreferences" + | "/api-reference/users/userRouter-changeDdgBangs" + | "/api-reference/users/userRouter-changeFirstDayOfWeek" + | "/api-reference/integrations/integration-request" + | "/blog/translations" + | "/blog/2024/09/23/version-1.0" + | "/blog/2024/12/17/open-beta-1.0" + | "/blog/documentation-migration" + | "/blog/2023/01/11/version0.11" + | "/blog/2023/04/16/version0.12-more-widgets" + | "/blog/2023/12/22/updated-documentation" + | "/blog/2023/11/10/authentication" + | "/blog/2025/01/19/migration-guide-1.0" + | "/blog/2026/09/03/homarr-2.0" + | "/blog/2025/08/02/using-argus" + | "/workshop" + | "/api-reference" + | "/about-us" | "/sitemap.xml" | "/docs/integrations/autobrr" | "/docs/integrations/jellystat" diff --git a/static-data/opencollective-contributors.json b/static-data/opencollective-contributors.json index 388670172..b58c72d25 100644 --- a/static-data/opencollective-contributors.json +++ b/static-data/opencollective-contributors.json @@ -1 +1 @@ -[{"id":"zaxon793-jy8gpl0v-d89pbrkd-emwl5v04","name":"Mike","slug":"guest-5da646e8","imageUrl":"https://images.opencollective.com/guest-5da646e8/avatar.png"},{"id":"ywz9j4av-god8pgwg-db36mr35-nxklb0e7","name":"Guest","slug":"guest-554ec68f","imageUrl":"https://images.opencollective.com/guest-554ec68f/avatar.png"},{"id":"mlo94zn7-x08dpo09-xlxpewga-3vjbrky5","name":"Robert Hermann","slug":"robert-hermann","imageUrl":"https://images.opencollective.com/robert-hermann/avatar.png"},{"id":"zaxon793-jy8gpl0n-8r3pbrkd-emwl5v04","name":"Sebastian Raubuch","slug":"sebastian-raubuch","imageUrl":"https://images.opencollective.com/sebastian-raubuch/avatar.png"},{"id":"nmlo94zn-7x08dpod-ykm6ewga-3vjbrky5","name":"Didier LUTHI","slug":"didier-luthi","imageUrl":"https://images.opencollective.com/didier-luthi/avatar.png"},{"id":"vedj9wro-z3a56dgk-z3z67blg-8x4m0ykn","name":"Guest","slug":"guest-b1459a68","imageUrl":"https://images.opencollective.com/guest-b1459a68/avatar.png"},{"id":"gm9bnk80-437xqry4-8vepvzeo-ljdayw5r","name":"Mohister","slug":"mohister","imageUrl":"https://images.opencollective.com/mohister/1d8a592/avatar.png"},{"id":"vrgbk357-l4x96e70-m0apomew-a0jdyzn8","name":"sidens","slug":"sidens","imageUrl":"https://images.opencollective.com/sidens/avatar.png"},{"id":"mlo94zn7-x08dpo0d-l7epewga-3vjbrky5","name":"Chris","slug":"guest-937065bb","imageUrl":"https://images.opencollective.com/guest-937065bb/avatar.png"},{"id":"k9mbw7y4-8r3zq3ol-7zlq0ej5-lavnodgx","name":"Nick W","slug":"guest-7501840a","imageUrl":"https://images.opencollective.com/guest-7501840a/avatar.png"},{"id":"e0adkjrv-8xwm69o8-5al67z5n-4l93bgoy","name":"Jeppedy","slug":"guest-ab78e46a","imageUrl":"https://images.opencollective.com/guest-ab78e46a/avatar.png"},{"id":"a47byg9n-xozdp8xo-jgm6mjlv-03rek5w8","name":"naru","slug":"guest-02fd264a","imageUrl":"https://images.opencollective.com/guest-02fd264a/avatar.png"},{"id":"3k0exgzn-m8yj64g3-ll865wao-9r7b4dlv","name":"Jared Hocutt","slug":"jared-hocutt","imageUrl":"https://images.opencollective.com/jared-hocutt/avatar.png"},{"id":"eng0kzdy-vor4pz8l-exopbma8-37xlw95j","name":"Incognito","slug":"user-e63f4a5b","imageUrl":"https://images.opencollective.com/user-e63f4a5b/avatar.png"},{"id":"8k03reyd-5agmq50v-8vyplbwo-z7j4nxv9","name":"dazi","slug":"guest-11663a46","imageUrl":"https://images.opencollective.com/guest-11663a46/avatar.png"},{"id":"vedj9wro-z3a56dgd-zwl67blg-8x4m0ykn","name":"ZoopaMario","slug":"zoopamario","imageUrl":"https://images.opencollective.com/zoopamario/bea900d/avatar.png"},{"id":"a47byg9n-xozdp8xe-xmb6mjlv-03rek5w8","name":"FlamebergUA","slug":"guest-a59fd500","imageUrl":"https://images.opencollective.com/guest-a59fd500/avatar.png"},{"id":"8k03reyd-5agmq50y-kzoplbwo-z7j4nxv9","name":"StLCards","slug":"guest-e1de33dc","imageUrl":"https://images.opencollective.com/guest-e1de33dc/avatar.png"},{"id":"ejoxl3az-45w9pa54-dk46y870-mgkbrvdn","name":"Varadha","slug":"guest-6f68eed9","imageUrl":"https://images.opencollective.com/guest-6f68eed9/avatar.png"},{"id":"jrkx5lmn-v904qjl0-3oz68bwa-7zdygoe3","name":"Guest","slug":"guest-6da419b5","imageUrl":"https://images.opencollective.com/guest-6da419b5/avatar.png"},{"id":"mywxoz34-09rl6k3z-lmlqvenb-dj7gk85a","name":"Guest","slug":"guest-122d7e0a","imageUrl":"https://images.opencollective.com/guest-122d7e0a/avatar.png"},{"id":"3z8arxve-ymko60vm-3eaqgl5n-bj9w704d","name":"Ricardo","slug":"ricardo34","imageUrl":"https://images.opencollective.com/ricardo34/avatar.png"},{"id":"e0adkjrv-8xwm69or-reo67z5n-4l93bgoy","name":"GoM","slug":"guest-3944feab","imageUrl":"https://images.opencollective.com/guest-3944feab/avatar.png"},{"id":"gm9bnk80-437xqrnr-r5o6vzeo-ljdayw5r","name":"Brent Maxwell","slug":"brentmaxwell","imageUrl":"https://images.opencollective.com/brentmaxwell/avatar.png"},{"id":"gnxdwzj3-le5mpwzd-ll0py8rv-bko04a97","name":"Eric G","slug":"guest-478270f1","imageUrl":"https://images.opencollective.com/guest-478270f1/avatar.png"},{"id":"v349mrwg-z75lpyjo-ez8pa08d-jeybknox","name":"ninja","slug":"guest-0bb6ba36","imageUrl":"https://images.opencollective.com/guest-0bb6ba36/avatar.png"},{"id":"ywz9j4av-god8pgw0-53y6mr35-nxklb0e7","name":"s11","slug":"guest-6e9a2588","imageUrl":"https://images.opencollective.com/guest-6e9a2588/avatar.png"},{"id":"mlo94zn7-x08dpo30-v73pewga-3vjbrky5","name":"Incognito","slug":"user-cbad11af","imageUrl":"https://images.opencollective.com/user-cbad11af/avatar.png"},{"id":"v349mrwg-z75lpy7l-yzgpa08d-jeybknox","name":"Ronald Simmons","slug":"ronald-simmons","imageUrl":"https://images.opencollective.com/ronald-simmons/avatar.png"},{"id":"7ywz9j4a-vgod8pgl-me9qmr35-nxklb0e7","name":"Joel","slug":"guest-3376ab02","imageUrl":"https://images.opencollective.com/guest-3376ab02/avatar.png"},{"id":"v349mrwg-z75lpyjw-kxzpa08d-jeybknox","name":"Gokay Yamankurt","slug":"gokay-yamankurt","imageUrl":"https://images.opencollective.com/gokay-yamankurt/avatar.png"},{"id":"v349mrwg-z75lpyjv-59rpa08d-jeybknox","name":"Rhiz3K","slug":"rhiz3k","imageUrl":"https://images.opencollective.com/rhiz3k/b4e9f9d/avatar.png"},{"id":"8k03reyd-5agmq5x7-77gplbwo-z7j4nxv9","name":"vukie","slug":"guest-40aabe13","imageUrl":"https://images.opencollective.com/guest-40aabe13/avatar.png"},{"id":"a47byg9n-xozdp84a-rmvqmjlv-03rek5w8","name":"Guest","slug":"guest-2fe979fa","imageUrl":"https://images.opencollective.com/guest-2fe979fa/avatar.png"},{"id":"rxg0j35l-zkwm6vgm-k3x6voe9-8n47daby","name":"AngryPig","slug":"guest-b9e8a062","imageUrl":"https://images.opencollective.com/guest-b9e8a062/avatar.png"},{"id":"acc_00XiGzC0V5uzYIL5imKK8","name":"Christopher Jones","slug":"christopher-jones-aae1b68a","imageUrl":"https://images.opencollective.com/christopher-jones-aae1b68a/avatar.png"},{"id":"acc_Usap6nzTGh8BCPg0V6lM7","name":"zzoomless7","slug":"guest-a4dec157","imageUrl":"https://images.opencollective.com/guest-a4dec157/avatar.png"},{"id":"acc_BhFQegbcujMZPebCsBuYv","name":"Sobe","slug":"guest-27946e70","imageUrl":"https://images.opencollective.com/guest-27946e70/avatar.png"},{"id":"acc_SvMxnHgtpBiOdXWGCkAfP","name":"Guest","slug":"guest-0e0259de","imageUrl":"https://images.opencollective.com/guest-0e0259de/avatar.png"}] \ No newline at end of file +[{"id":"zaxon793-jy8gpl0v-d89pbrkd-emwl5v04","name":"Mike","slug":"guest-5da646e8","imageUrl":"https://images.opencollective.com/guest-5da646e8/avatar.png"},{"id":"ywz9j4av-god8pgwg-db36mr35-nxklb0e7","name":"Guest","slug":"guest-554ec68f","imageUrl":"https://images.opencollective.com/guest-554ec68f/avatar.png"},{"id":"mlo94zn7-x08dpo09-xlxpewga-3vjbrky5","name":"Robert Hermann","slug":"robert-hermann","imageUrl":"https://images.opencollective.com/robert-hermann/avatar.png"},{"id":"zaxon793-jy8gpl0n-8r3pbrkd-emwl5v04","name":"Sebastian Raubuch","slug":"sebastian-raubuch","imageUrl":"https://images.opencollective.com/sebastian-raubuch/avatar.png"},{"id":"nmlo94zn-7x08dpod-ykm6ewga-3vjbrky5","name":"Didier LUTHI","slug":"didier-luthi","imageUrl":"https://images.opencollective.com/didier-luthi/avatar.png"},{"id":"vedj9wro-z3a56dgk-z3z67blg-8x4m0ykn","name":"Guest","slug":"guest-b1459a68","imageUrl":"https://images.opencollective.com/guest-b1459a68/avatar.png"},{"id":"gm9bnk80-437xqry4-8vepvzeo-ljdayw5r","name":"Mohister","slug":"mohister","imageUrl":"https://images.opencollective.com/mohister/1d8a592/avatar.png"},{"id":"vrgbk357-l4x96e70-m0apomew-a0jdyzn8","name":"sidens","slug":"sidens","imageUrl":"https://images.opencollective.com/sidens/avatar.png"},{"id":"mlo94zn7-x08dpo0d-l7epewga-3vjbrky5","name":"Chris","slug":"guest-937065bb","imageUrl":"https://images.opencollective.com/guest-937065bb/avatar.png"},{"id":"k9mbw7y4-8r3zq3ol-7zlq0ej5-lavnodgx","name":"Nick W","slug":"guest-7501840a","imageUrl":"https://images.opencollective.com/guest-7501840a/avatar.png"},{"id":"e0adkjrv-8xwm69o8-5al67z5n-4l93bgoy","name":"Jeppedy","slug":"guest-ab78e46a","imageUrl":"https://images.opencollective.com/guest-ab78e46a/avatar.png"},{"id":"a47byg9n-xozdp8xo-jgm6mjlv-03rek5w8","name":"naru","slug":"guest-02fd264a","imageUrl":"https://images.opencollective.com/guest-02fd264a/avatar.png"},{"id":"3k0exgzn-m8yj64g3-ll865wao-9r7b4dlv","name":"Jared Hocutt","slug":"jared-hocutt","imageUrl":"https://images.opencollective.com/jared-hocutt/avatar.png"},{"id":"eng0kzdy-vor4pz8l-exopbma8-37xlw95j","name":"Incognito","slug":"user-e63f4a5b","imageUrl":"https://images.opencollective.com/user-e63f4a5b/avatar.png"},{"id":"8k03reyd-5agmq50v-8vyplbwo-z7j4nxv9","name":"dazi","slug":"guest-11663a46","imageUrl":"https://images.opencollective.com/guest-11663a46/avatar.png"},{"id":"vedj9wro-z3a56dgd-zwl67blg-8x4m0ykn","name":"ZoopaMario","slug":"zoopamario","imageUrl":"https://images.opencollective.com/zoopamario/bea900d/avatar.png"},{"id":"a47byg9n-xozdp8xe-xmb6mjlv-03rek5w8","name":"FlamebergUA","slug":"guest-a59fd500","imageUrl":"https://images.opencollective.com/guest-a59fd500/avatar.png"},{"id":"8k03reyd-5agmq50y-kzoplbwo-z7j4nxv9","name":"StLCards","slug":"guest-e1de33dc","imageUrl":"https://images.opencollective.com/guest-e1de33dc/avatar.png"},{"id":"ejoxl3az-45w9pa54-dk46y870-mgkbrvdn","name":"Varadha","slug":"guest-6f68eed9","imageUrl":"https://images.opencollective.com/guest-6f68eed9/avatar.png"},{"id":"jrkx5lmn-v904qjl0-3oz68bwa-7zdygoe3","name":"Guest","slug":"guest-6da419b5","imageUrl":"https://images.opencollective.com/guest-6da419b5/avatar.png"},{"id":"mywxoz34-09rl6k3z-lmlqvenb-dj7gk85a","name":"Guest","slug":"guest-122d7e0a","imageUrl":"https://images.opencollective.com/guest-122d7e0a/avatar.png"},{"id":"3z8arxve-ymko60vm-3eaqgl5n-bj9w704d","name":"Ricardo","slug":"ricardo34","imageUrl":"https://images.opencollective.com/ricardo34/avatar.png"},{"id":"e0adkjrv-8xwm69or-reo67z5n-4l93bgoy","name":"GoM","slug":"guest-3944feab","imageUrl":"https://images.opencollective.com/guest-3944feab/avatar.png"},{"id":"gm9bnk80-437xqrnr-r5o6vzeo-ljdayw5r","name":"Brent Maxwell","slug":"brentmaxwell","imageUrl":"https://images.opencollective.com/brentmaxwell/avatar.png"},{"id":"gnxdwzj3-le5mpwzd-ll0py8rv-bko04a97","name":"Eric G","slug":"guest-478270f1","imageUrl":"https://images.opencollective.com/guest-478270f1/avatar.png"},{"id":"v349mrwg-z75lpyjo-ez8pa08d-jeybknox","name":"ninja","slug":"guest-0bb6ba36","imageUrl":"https://images.opencollective.com/guest-0bb6ba36/avatar.png"},{"id":"ywz9j4av-god8pgw0-53y6mr35-nxklb0e7","name":"s11","slug":"guest-6e9a2588","imageUrl":"https://images.opencollective.com/guest-6e9a2588/avatar.png"},{"id":"mlo94zn7-x08dpo30-v73pewga-3vjbrky5","name":"Incognito","slug":"user-cbad11af","imageUrl":"https://images.opencollective.com/user-cbad11af/avatar.png"},{"id":"v349mrwg-z75lpy7l-yzgpa08d-jeybknox","name":"Ronald Simmons","slug":"ronald-simmons","imageUrl":"https://images.opencollective.com/ronald-simmons/avatar.png"},{"id":"7ywz9j4a-vgod8pgl-me9qmr35-nxklb0e7","name":"Joel","slug":"guest-3376ab02","imageUrl":"https://images.opencollective.com/guest-3376ab02/avatar.png"},{"id":"v349mrwg-z75lpyjw-kxzpa08d-jeybknox","name":"Gokay Yamankurt","slug":"gokay-yamankurt","imageUrl":"https://images.opencollective.com/gokay-yamankurt/avatar.png"},{"id":"v349mrwg-z75lpyjv-59rpa08d-jeybknox","name":"Rhiz3K","slug":"rhiz3k","imageUrl":"https://images.opencollective.com/rhiz3k/b4e9f9d/avatar.png"},{"id":"8k03reyd-5agmq5x7-77gplbwo-z7j4nxv9","name":"vukie","slug":"guest-40aabe13","imageUrl":"https://images.opencollective.com/guest-40aabe13/avatar.png"},{"id":"a47byg9n-xozdp84a-rmvqmjlv-03rek5w8","name":"Guest","slug":"guest-2fe979fa","imageUrl":"https://images.opencollective.com/guest-2fe979fa/avatar.png"},{"id":"rxg0j35l-zkwm6vgm-k3x6voe9-8n47daby","name":"AngryPig","slug":"guest-b9e8a062","imageUrl":"https://images.opencollective.com/guest-b9e8a062/avatar.png"},{"id":"acc_00XiGzC0V5uzYIL5imKK8","name":"Christopher Jones","slug":"christopher-jones-aae1b68a","imageUrl":"https://images.opencollective.com/christopher-jones-aae1b68a/avatar.png"},{"id":"acc_Usap6nzTGh8BCPg0V6lM7","name":"zzoomless7","slug":"guest-a4dec157","imageUrl":"https://images.opencollective.com/guest-a4dec157/avatar.png"},{"id":"acc_BhFQegbcujMZPebCsBuYv","name":"Sobe","slug":"guest-27946e70","imageUrl":"https://images.opencollective.com/guest-27946e70/avatar.png"},{"id":"acc_SvMxnHgtpBiOdXWGCkAfP","name":"","slug":"guest-0e0259de","imageUrl":"https://images.opencollective.com/guest-0e0259de/avatar.png"}] \ No newline at end of file diff --git a/static-data/translators.json b/static-data/translators.json index 8a5a8e7ef..7d702aad7 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":"RollstuhlJack","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12386691/medium/192fa7d350b148a55d37332c9d17c529_default.png"},{"username":"ArcaneWater","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12421105/medium/1ac82517a6c658f6b62c48aebcb17e86_default.png"},{"username":"Ascalon","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12743376/medium/7845361ea1fae14e71978df906d60250.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":"thrmda","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13431074/medium/c79736af3a4140a01254ea1a887f64bc_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":"rimasx","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13578080/medium/c356991f3a0c2cfadb22e62cab0cd1bd.png"},{"username":"Cruellest","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13610615/medium/2a74faa0947ee08d18f7946fb8602c1e.jpg"},{"username":"colonnelli.st","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13835985/medium/69aac81cc1475cff06fff0dc6ee7ad51.jpg"},{"username":"Fabian9895","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14188755/medium/db4ddd4064bfe3a2b7aeb0f0e2952f0d.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":"samuelinho","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14666416/medium/6e0535b810a65d737d39c9cbfc973f7b.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/41a6a5b4c31c125385ed16dc64bcdc3e.jpg"},{"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":"levnetet","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15149710/medium/9f96f712f64a8f6d195e6ed81d5f1313_default.png"},{"username":"legocuedy09","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15206280/medium/a93129b77a54ffbb2c9f136f18718e80.png"},{"username":"catvari","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15442102/medium/f5cbda04afc08aa9134878cbd1f7db86.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":"duck-overflow","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16718713/medium/b84756c0e3ac55807812d166a105adda.jpeg"},{"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":"danura30082","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16781783/medium/17923be963d2a69a5f3b0c81eebb7fb4.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"},{"username":"agiledivider","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17801131/medium/0039d926d5a716e712e36b06004369ab_default.png"},{"username":"nocteln","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17806203/medium/fc84143ba6b94cc8e8a37415a0d91fb0_default.png"},{"username":"b.yatsenko","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17810347/medium/6db1ce1d6b9f07afbcec0ca43b8015c8.png"},{"username":"wolliS","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17824457/medium/ab196ecab415d34826955027ae36a72f_default.png"},{"username":"agentusmentus","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17826645/medium/2ccb13073c04fba9cc4ee65cf879110a.png"},{"username":"--AMT--","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17826763/medium/265bab26069472b490520acc75e42f71_default.png"},{"username":"aleh1771","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17828077/medium/2408df7f4b85a3294a0ceed8895d5239.png"},{"username":"dmmc","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17832649/medium/5ad460dcf35f39f4c6567e911a8a3abb.jpeg"},{"username":"riscurred","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17837663/medium/7cb18f3e9f58e8f946b9bc454fd1e60e.jpeg"},{"username":"WeXet","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17853639/medium/5b0e29d3a589f4cf67bbed528d51eef2.png"},{"username":"otiro","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17859031/medium/70379ab92c0baec710d783177b9b9723.png"},{"username":"gulugumi","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13430223/medium/d77ab1a40ba6a9ae54901196d56c9852.jpg"},{"username":"hhveerman","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13881349/medium/5d0090ede7b62c6c1f3486ea1cef2266.jpg"},{"username":"StickyNicky","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17871927/medium/766e954f9470594430d9e2ba26a5e997_default.png"}] +[{"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":"RollstuhlJack","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12386691/medium/192fa7d350b148a55d37332c9d17c529_default.png"},{"username":"ArcaneWater","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12421105/medium/1ac82517a6c658f6b62c48aebcb17e86_default.png"},{"username":"Ascalon","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12743376/medium/7845361ea1fae14e71978df906d60250.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":"gulugumi","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13430223/medium/d77ab1a40ba6a9ae54901196d56c9852.jpg"},{"username":"thrmda","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13431074/medium/c79736af3a4140a01254ea1a887f64bc_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":"rimasx","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13578080/medium/c356991f3a0c2cfadb22e62cab0cd1bd.png"},{"username":"Cruellest","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13610615/medium/2a74faa0947ee08d18f7946fb8602c1e.jpg"},{"username":"colonnelli.st","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13835985/medium/69aac81cc1475cff06fff0dc6ee7ad51.jpg"},{"username":"hhveerman","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13881349/medium/5d0090ede7b62c6c1f3486ea1cef2266.jpg"},{"username":"Fabian9895","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14188755/medium/db4ddd4064bfe3a2b7aeb0f0e2952f0d.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":"samuelinho","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14666416/medium/6e0535b810a65d737d39c9cbfc973f7b.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":"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":"levnetet","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15149710/medium/9f96f712f64a8f6d195e6ed81d5f1313_default.png"},{"username":"legocuedy09","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15206280/medium/a93129b77a54ffbb2c9f136f18718e80.png"},{"username":"catvari","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15442102/medium/f5cbda04afc08aa9134878cbd1f7db86.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":"duck-overflow","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16718713/medium/b84756c0e3ac55807812d166a105adda.jpeg"},{"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":"danura30082","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16781783/medium/17923be963d2a69a5f3b0c81eebb7fb4.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"},{"username":"agiledivider","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17801131/medium/0039d926d5a716e712e36b06004369ab_default.png"},{"username":"nocteln","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17806203/medium/fc84143ba6b94cc8e8a37415a0d91fb0_default.png"},{"username":"b.yatsenko","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17810347/medium/6db1ce1d6b9f07afbcec0ca43b8015c8.png"},{"username":"wolliS","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17824457/medium/ab196ecab415d34826955027ae36a72f_default.png"},{"username":"agentusmentus","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17826645/medium/2ccb13073c04fba9cc4ee65cf879110a.png"},{"username":"--AMT--","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17826763/medium/265bab26069472b490520acc75e42f71_default.png"},{"username":"aleh1771","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17828077/medium/2408df7f4b85a3294a0ceed8895d5239.png"},{"username":"dmmc","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17832649/medium/5ad460dcf35f39f4c6567e911a8a3abb.jpeg"},{"username":"riscurred","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17837663/medium/7cb18f3e9f58e8f946b9bc454fd1e60e.jpeg"},{"username":"WeXet","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17853639/medium/5b0e29d3a589f4cf67bbed528d51eef2.png"},{"username":"otiro","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17859031/medium/70379ab92c0baec710d783177b9b9723.png"},{"username":"StickyNicky","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17871927/medium/766e954f9470594430d9e2ba26a5e997_default.png"},{"username":"shimmyx","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17920973/medium/c5e735d073e8fbe4c357e21ffff10e72.png"}] From 5c2b6b37d8e0517bc0bcae994e7bda96a1e48d43 Mon Sep 17 00:00:00 2001 From: "homarr-renovate[bot]" <158783068+homarr-renovate[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 19:56:52 +0000 Subject: [PATCH 02/29] chore(deps): update dependency brace-expansion@>=4.0.0 <5.0.9 to v5.0.12 [security] (#6958) Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 22 +++++++++++++++++----- pnpm-workspace.yaml | 2 +- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 576242d4c..bd83f6581 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -714,7 +714,7 @@ overrides: axios@>=1.0.0 <1.18.1: 1.18.1 brace-expansion@>=1.0.0 <1.1.18: 1.1.18 brace-expansion@>=2.0.0 <2.1.4: 2.1.4 - brace-expansion@>=4.0.0 <5.0.9: 5.0.9 + brace-expansion@>=4.0.0 <5.0.9: 5.0.12 browserslist@<4.28.7: 4.28.7 esbuild@<=0.24.2: '>=0.28.1' fast-uri@>=3.0.0 <3.1.6: 3.1.6 @@ -8496,67 +8496,79 @@ packages: resolution: {integrity: sha512-xt4Vy3RTBw1NfszBlf4A0Rr6ZjA/th2Pj9XlWFLQHW9A1qOCtxNCoIWSzn/INtnZLMB4jO2gWdEtSaHUtK73wA==} cpu: [arm64] os: [android] + deprecated: yuku-analyzer runs on yuku-core since 0.14 '@yuku-analyzer/binding-darwin-arm64@0.10.2': resolution: {integrity: sha512-ftokSqvYXZxfmA/bfIbp8c1BJ8QxNax371kUOQNFyvR5hmtKqQyTqSKtMk+WaixHM3w4tKj/JdyY+ud1bGamXA==} cpu: [arm64] os: [darwin] + deprecated: yuku-analyzer runs on yuku-core since 0.14 '@yuku-analyzer/binding-darwin-x64@0.10.2': resolution: {integrity: sha512-yUrmmhmF5R4Xmbfo8gUxJKr3YbvbLcE2JvLhDLzFJkMN29/fGm6K3dZX6YpNecfgBDnQ+VywGp8K9xjRaBFZ3Q==} cpu: [x64] os: [darwin] + deprecated: yuku-analyzer runs on yuku-core since 0.14 '@yuku-analyzer/binding-freebsd-x64@0.10.2': resolution: {integrity: sha512-cy9+KvVoK5Xad3Vs5pLwAeZf3JC/1y0XJElgv1q6g7W7g/n52FRjedpYmQiJeMZdnhJiAshq7Vz/Xj3jC5Ns2Q==} cpu: [x64] os: [freebsd] + deprecated: yuku-analyzer runs on yuku-core since 0.14 '@yuku-analyzer/binding-linux-arm-gnu@0.10.2': resolution: {integrity: sha512-AgVZ6VCV2XP4yHCwyv78KLV76gcu2neiEF2+RBTPQN7sqYY+bLvMxphGPmfG+9qk5Uf1PJAhZYIUnMGVbbRCZw==} cpu: [arm] os: [linux] libc: [glibc] + deprecated: yuku-analyzer runs on yuku-core since 0.14 '@yuku-analyzer/binding-linux-arm-musl@0.10.2': resolution: {integrity: sha512-5qAj2GjoSN6+HuH75+g5r40CFhWmMvH0K8VKWmPfQyOgA/LonoDMr5H5O5feJwzdnfdCytPDAh1RAf7/SSg7dg==} cpu: [arm] os: [linux] libc: [musl] + deprecated: yuku-analyzer runs on yuku-core since 0.14 '@yuku-analyzer/binding-linux-arm64-gnu@0.10.2': resolution: {integrity: sha512-eJvQ4Ja5oPf/HOjmBkCLYyhOHUkL7U+iMAN8sX44x8exBBpPRyqnhEgTsuU73HzxPLrGPdRhybFnAE63Omib/Q==} cpu: [arm64] os: [linux] libc: [glibc] + deprecated: yuku-analyzer runs on yuku-core since 0.14 '@yuku-analyzer/binding-linux-arm64-musl@0.10.2': resolution: {integrity: sha512-TppF2j194mOQ/kBXvNr9u5TYO+eR9r8ieg4lTIEyJIVYHAR9GWeB0oFHLcJiLXVa3qUJGzE4bmzSntCW5ylA7A==} cpu: [arm64] os: [linux] libc: [musl] + deprecated: yuku-analyzer runs on yuku-core since 0.14 '@yuku-analyzer/binding-linux-x64-gnu@0.10.2': resolution: {integrity: sha512-03xu6ocJuwhm1a/JKmrHiaJUO1Oj86rABsm4FAl8AvfewfW/9idIUmPiFxZfb6RfiIONegebsTGJhFx0NbBNpA==} cpu: [x64] os: [linux] libc: [glibc] + deprecated: yuku-analyzer runs on yuku-core since 0.14 '@yuku-analyzer/binding-linux-x64-musl@0.10.2': resolution: {integrity: sha512-f7W2sAWWQN8x8YB0RRxIche7mLImH7lDgzUy9+B69cM2I6VFJ4a5+0pgAuMLROPOqimaxFPdXqLdDE/vpgGltg==} cpu: [x64] os: [linux] libc: [musl] + deprecated: yuku-analyzer runs on yuku-core since 0.14 '@yuku-analyzer/binding-win32-arm64@0.10.2': resolution: {integrity: sha512-f534qNc5YaJI29e1oryxEnxPkp7WCYJ1j7ChSec8pQNlYqTm8Tao7hij7NxRF+9BuxzeZCZ+WsbaAfLlklR0Eg==} cpu: [arm64] os: [win32] + deprecated: yuku-analyzer runs on yuku-core since 0.14 '@yuku-analyzer/binding-win32-x64@0.10.2': resolution: {integrity: sha512-sKHkmLK8BsindMfyIThoN0bWC5uqSJEVNaLRD/ifA99mtb0UDin1ZCg4WYSVQAmM4k2iOTl4UZx/p3zcBiBgYw==} cpu: [x64] os: [win32] + deprecated: yuku-analyzer runs on yuku-core since 0.14 '@yuku-toolchain/types@0.10.2': resolution: {integrity: sha512-sSeo4SSSToiS+sSD+bwn/s94EEcaLJ7tG5LCp8gFYC1G5VzxzX7fqB6m9RU1yMD8KTpu6zdU/I1NlQf8hVBJ9Q==} @@ -8857,8 +8869,8 @@ packages: brace-expansion@2.1.4: resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} - brace-expansion@5.0.9: - resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} engines: {node: 20 || >=22} braces@3.0.3: @@ -20221,7 +20233,7 @@ snapshots: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.9: + brace-expansion@5.0.12: dependencies: balanced-match: 4.0.2 @@ -23349,7 +23361,7 @@ snapshots: minimatch@10.2.5: dependencies: - brace-expansion: 5.0.9 + brace-expansion: 5.0.12 minimatch@3.1.5: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 52e56a214..f83d93d35 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -270,7 +270,7 @@ overrides: "axios@>=1.0.0 <1.18.1": "1.18.1" "brace-expansion@>=1.0.0 <1.1.18": "1.1.18" "brace-expansion@>=2.0.0 <2.1.4": "2.1.4" - "brace-expansion@>=4.0.0 <5.0.9": "5.0.9" + "brace-expansion@>=4.0.0 <5.0.9": "5.0.12" "browserslist@<4.28.7": "4.28.7" "esbuild@<=0.24.2": ">=0.28.1" "fast-uri@>=3.0.0 <3.1.6": "3.1.6" From ce29c8627fdc96ceec03a782fb73d383d5345670 Mon Sep 17 00:00:00 2001 From: "homarr-renovate[bot]" <158783068+homarr-renovate[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 20:03:27 +0000 Subject: [PATCH 03/29] fix(deps): update dependency mysql2 to v3.23.1 [security] (#6965) Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com> --- tools/mysql-to-sqlite/package-lock.json | 10 +++++----- tools/mysql-to-sqlite/package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tools/mysql-to-sqlite/package-lock.json b/tools/mysql-to-sqlite/package-lock.json index 7f231901e..2610b3b03 100644 --- a/tools/mysql-to-sqlite/package-lock.json +++ b/tools/mysql-to-sqlite/package-lock.json @@ -8,7 +8,7 @@ "name": "homarr-mysql-to-sqlite", "version": "1.0.0", "dependencies": { - "mysql2": "3.22.6" + "mysql2": "3.23.1" }, "engines": { "node": ">=24.18.0 <25" @@ -95,9 +95,9 @@ } }, "node_modules/mysql2": { - "version": "3.22.6", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.22.6.tgz", - "integrity": "sha512-fPKmeDGUzvFP7bMD5SASlJ5zIgvCC4hbanTmhbUlEmhyrY1hR4Hi3xLOWgTd3luYjLVifx6uGvMNJ2m/LlqEpg==", + "version": "3.23.1", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.23.1.tgz", + "integrity": "sha512-tTuRnC7qCet2IOfSNMYZ5SwXuBnfvBPAcIA28P0gtruXyZlU1LMxA6uha32kYypoFgyYklMqhLWwt4laYwXR/Q==", "license": "MIT", "dependencies": { "aws-ssl-profiles": "^1.1.2", @@ -107,7 +107,7 @@ "long": "^5.3.2", "lru.min": "^1.1.4", "named-placeholders": "^1.1.6", - "sql-escaper": "^1.4.0" + "sql-escaper": "^1.5.1" }, "engines": { "node": ">= 8.0" diff --git a/tools/mysql-to-sqlite/package.json b/tools/mysql-to-sqlite/package.json index 99b9dc387..9b975e7b5 100644 --- a/tools/mysql-to-sqlite/package.json +++ b/tools/mysql-to-sqlite/package.json @@ -8,7 +8,7 @@ "test": "node --test test/*.test.mjs" }, "dependencies": { - "mysql2": "3.22.6" + "mysql2": "3.23.1" }, "engines": { "node": ">=24.18.0 <25" From 35baf9938efa10de57aaa39568e04960cc938200 Mon Sep 17 00:00:00 2001 From: "homarr-renovate[bot]" <158783068+homarr-renovate[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 20:04:23 +0000 Subject: [PATCH 04/29] chore(deps): update dependency fast-uri@>=3.0.0 <3.1.6 to v3.1.8 [security] (#6959) Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 14 +++++++------- pnpm-workspace.yaml | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd83f6581..49bc1346d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -717,7 +717,7 @@ overrides: brace-expansion@>=4.0.0 <5.0.9: 5.0.12 browserslist@<4.28.7: 4.28.7 esbuild@<=0.24.2: '>=0.28.1' - fast-uri@>=3.0.0 <3.1.6: 3.1.6 + fast-uri@>=3.0.0 <3.1.6: 3.1.8 '@ai-sdk/provider-utils>undici': 8.10.2 find-my-way@<9.6.1: 9.9.0 form-data@>=4.0.0 <4.0.4: '>=4.0.6' @@ -10046,8 +10046,8 @@ packages: fast-string-width@3.0.2: resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} - fast-uri@3.1.6: - resolution: {integrity: sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==} + fast-uri@3.1.8: + resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==} fast-uri@4.2.1: resolution: {integrity: sha512-TmHQgewjHtMq1E5QKA0tOE0yeYGQs25KZC/ziJpubRtWI15W92e6vPFydWOeZBVROSHBYw/QhD9d5OefdD6LDg==} @@ -15409,7 +15409,7 @@ snapshots: dependencies: ajv: 8.18.0 ajv-formats: 3.0.1(ajv@8.18.0) - fast-uri: 3.1.6 + fast-uri: 3.1.8 '@fastify/error@4.2.0': {} @@ -19990,7 +19990,7 @@ snapshots: ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.6 + fast-uri: 3.1.8 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -21514,7 +21514,7 @@ snapshots: '@fastify/merge-json-schemas': 0.2.1 ajv: 8.18.0 ajv-formats: 3.0.1(ajv@8.18.0) - fast-uri: 3.1.6 + fast-uri: 3.1.8 json-schema-ref-resolver: 3.0.0 rfdc: 1.4.1 @@ -21539,7 +21539,7 @@ snapshots: fast-string-truncated-width: 3.0.3 optional: true - fast-uri@3.1.6: {} + fast-uri@3.1.8: {} fast-uri@4.2.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f83d93d35..fe32a86b8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -273,7 +273,7 @@ overrides: "brace-expansion@>=4.0.0 <5.0.9": "5.0.12" "browserslist@<4.28.7": "4.28.7" "esbuild@<=0.24.2": ">=0.28.1" - "fast-uri@>=3.0.0 <3.1.6": "3.1.6" + "fast-uri@>=3.0.0 <3.1.6": "3.1.8" "@ai-sdk/provider-utils>undici": "8.10.2" "find-my-way@<9.6.1": "9.9.0" "form-data@>=4.0.0 <4.0.4": ">=4.0.6" From 8a177c5d0fc791b94f441c13d81c4a2849affd2b Mon Sep 17 00:00:00 2001 From: "homarr-renovate[bot]" <158783068+homarr-renovate[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 20:08:55 +0000 Subject: [PATCH 05/29] chore(deps): update dependency axios@>=1.0.0 <1.18.1 to v1.20.0 [security] (#6962) Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 44 ++++++++++++++++++++++---------------------- pnpm-workspace.yaml | 2 +- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49bc1346d..37159e08f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -711,7 +711,7 @@ overrides: assistant-cloud: 0.1.38 '@babel/helpers@<7.26.10': '>=7.29.7' '@babel/runtime@<7.26.10': '>=7.29.7' - axios@>=1.0.0 <1.18.1: 1.18.1 + axios@>=1.0.0 <1.18.1: 1.20.0 brace-expansion@>=1.0.0 <1.1.18: 1.1.18 brace-expansion@>=2.0.0 <2.1.4: 2.1.4 brace-expansion@>=4.0.0 <5.0.9: 5.0.12 @@ -873,7 +873,7 @@ importers: version: 0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@scalar/api-client-react': specifier: 'catalog:' - version: 2.0.51(axios@1.18.1(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2))(nprogress@0.2.0)(react@19.2.7)(supports-color@10.2.2)(tailwindcss@4.3.2)(typescript@7.0.2) + version: 2.0.51(axios@1.20.0(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2))(nprogress@0.2.0)(react@19.2.7)(supports-color@10.2.2)(tailwindcss@4.3.2)(typescript@7.0.2) '@tabler/icons-react': specifier: 'catalog:' version: 3.44.0(react@19.2.7) @@ -903,7 +903,7 @@ importers: version: 15.4.3(ca407bc45757862c0553f46d1969d5d6) fumadocs-openapi: specifier: 'catalog:' - version: 12.0.1(2b987dc9753a22b34010c9167028a29c) + version: 12.0.1(18308fb02f1370e08bd00c000219c030) fumadocs-ui: specifier: 'catalog:' version: 16.15.12(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.15.12(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@modelcontextprotocol/server@2.0.0)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(algoliasearch@5.53.0)(lucide-react@1.47.0(react@19.2.7))(next@16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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)(supports-color@10.2.2)(zod@4.4.3))(next@16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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)(tailwindcss@4.3.2) @@ -2308,7 +2308,7 @@ importers: version: 3.0.2 '@jellyfin/sdk': specifier: 'catalog:' - version: 0.13.0(axios@1.18.1(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2)) + version: 0.13.0(axios@1.20.0(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2)) ical.js: specifier: 'catalog:' version: 2.2.1 @@ -4903,7 +4903,7 @@ packages: '@jellyfin/sdk@0.13.0': resolution: {integrity: sha512-oiBAOXH6s+dKdReSsYgNktBDzbxtg4JVWhEzIxZSxKcWMdSKmBtK41MhXRO7IWAC40DguKUm3nU/Z493qPAlWA==} peerDependencies: - axios: 1.18.1 + axios: 1.20.0 '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -8368,7 +8368,7 @@ packages: resolution: {integrity: sha512-SDobKBbPIOe0cVL7QxMzGkuUGHvWTdihi9zOrrWaWUgFKe15cwEcwfWmgrcNzjT6kHnNmWuTajPHoIzUjYNYYQ==} peerDependencies: async-validator: ^4 - axios: 1.18.1 + axios: 1.20.0 change-case: ^5 drauu: ^0.4 focus-trap: ^7 @@ -8767,8 +8767,8 @@ packages: avvio@9.2.0: resolution: {integrity: sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==} - axios@1.18.1: - resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==} + axios@1.20.0: + resolution: {integrity: sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==} b4a@1.6.6: resolution: {integrity: sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==} @@ -15605,7 +15605,7 @@ snapshots: '@homarr/node-unifi@2.6.0(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2)(undici@7.29.1)': dependencies: - axios: 1.18.1(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2) + axios: 1.20.0(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2) eventemitter2: 6.4.9 http-cookie-agent: 6.0.8(tough-cookie@5.1.2)(undici@7.29.1) tough-cookie: 5.1.2 @@ -15914,9 +15914,9 @@ snapshots: '@isaacs/cliui@9.0.0': {} - '@jellyfin/sdk@0.13.0(axios@1.18.1(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2))': + '@jellyfin/sdk@0.13.0(axios@1.20.0(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2))': dependencies: - axios: 1.18.1(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2) + axios: 1.20.0(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2) '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -17877,7 +17877,7 @@ snapshots: '@scalar/agent-chat@0.12.37(supports-color@10.2.2)(tailwindcss@4.3.2)(typescript@7.0.2)(zod@4.4.3)': dependencies: '@ai-sdk/vue': 3.0.33(vue@3.5.42(typescript@7.0.2))(zod@4.4.3) - '@scalar/api-client': 3.21.1(axios@1.18.1(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2))(nprogress@0.2.0)(supports-color@10.2.2)(tailwindcss@4.3.2)(typescript@7.0.2) + '@scalar/api-client': 3.21.1(axios@1.20.0(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2))(nprogress@0.2.0)(supports-color@10.2.2)(tailwindcss@4.3.2)(typescript@7.0.2) '@scalar/components': 0.30.3(supports-color@10.2.2)(tailwindcss@4.3.2)(typescript@7.0.2) '@scalar/helpers': 0.15.0 '@scalar/icons': 0.7.6(typescript@7.0.2) @@ -17912,9 +17912,9 @@ snapshots: - universal-cookie - zod - '@scalar/api-client-react@2.0.51(axios@1.18.1(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2))(nprogress@0.2.0)(react@19.2.7)(supports-color@10.2.2)(tailwindcss@4.3.2)(typescript@7.0.2)': + '@scalar/api-client-react@2.0.51(axios@1.20.0(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2))(nprogress@0.2.0)(react@19.2.7)(supports-color@10.2.2)(tailwindcss@4.3.2)(typescript@7.0.2)': dependencies: - '@scalar/api-client': 3.21.1(axios@1.18.1(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2))(nprogress@0.2.0)(supports-color@10.2.2)(tailwindcss@4.3.2)(typescript@7.0.2) + '@scalar/api-client': 3.21.1(axios@1.20.0(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2))(nprogress@0.2.0)(supports-color@10.2.2)(tailwindcss@4.3.2)(typescript@7.0.2) '@scalar/blocks': 0.4.0(supports-color@10.2.2)(tailwindcss@4.3.2)(typescript@7.0.2) '@scalar/helpers': 0.15.0 '@scalar/workspace-store': 0.67.0(typescript@7.0.2) @@ -17935,7 +17935,7 @@ snapshots: - typescript - universal-cookie - '@scalar/api-client@3.21.1(axios@1.18.1(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2))(nprogress@0.2.0)(supports-color@10.2.2)(tailwindcss@4.3.2)(typescript@7.0.2)': + '@scalar/api-client@3.21.1(axios@1.20.0(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2))(nprogress@0.2.0)(supports-color@10.2.2)(tailwindcss@4.3.2)(typescript@7.0.2)': dependencies: '@headlessui/tailwindcss': 0.2.2(tailwindcss@4.3.2) '@headlessui/vue': 1.7.23(vue@3.5.42(typescript@7.0.2)) @@ -17956,7 +17956,7 @@ snapshots: '@scalar/use-toasts': 0.10.5(typescript@7.0.2) '@scalar/workspace-store': 0.67.0(typescript@7.0.2) '@vueuse/core': 13.9.0(vue@3.5.42(typescript@7.0.2)) - '@vueuse/integrations': 13.9.0(axios@1.18.1(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2))(focus-trap@7.8.0)(fuse.js@7.5.0)(nprogress@0.2.0)(vue@3.5.42(typescript@7.0.2)) + '@vueuse/integrations': 13.9.0(axios@1.20.0(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2))(focus-trap@7.8.0)(fuse.js@7.5.0)(nprogress@0.2.0)(vue@3.5.42(typescript@7.0.2)) focus-trap: 7.8.0 fuse.js: 7.5.0 js-base64: 3.9.3 @@ -18010,7 +18010,7 @@ snapshots: dependencies: '@headlessui/vue': 1.7.23(vue@3.5.42(typescript@7.0.2)) '@scalar/agent-chat': 0.12.37(supports-color@10.2.2)(tailwindcss@4.3.2)(typescript@7.0.2)(zod@4.4.3) - '@scalar/api-client': 3.21.1(axios@1.18.1(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2))(nprogress@0.2.0)(supports-color@10.2.2)(tailwindcss@4.3.2)(typescript@7.0.2) + '@scalar/api-client': 3.21.1(axios@1.20.0(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2))(nprogress@0.2.0)(supports-color@10.2.2)(tailwindcss@4.3.2)(typescript@7.0.2) '@scalar/blocks': 0.4.0(supports-color@10.2.2)(tailwindcss@4.3.2)(typescript@7.0.2) '@scalar/code-highlight': 0.4.7(supports-color@10.2.2) '@scalar/components': 0.30.3(supports-color@10.2.2)(tailwindcss@4.3.2)(typescript@7.0.2) @@ -19759,13 +19759,13 @@ snapshots: '@vueuse/shared': 13.9.0(vue@3.5.42(typescript@7.0.2)) vue: 3.5.42(typescript@7.0.2) - '@vueuse/integrations@13.9.0(axios@1.18.1(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2))(focus-trap@7.8.0)(fuse.js@7.5.0)(nprogress@0.2.0)(vue@3.5.42(typescript@7.0.2))': + '@vueuse/integrations@13.9.0(axios@1.20.0(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2))(focus-trap@7.8.0)(fuse.js@7.5.0)(nprogress@0.2.0)(vue@3.5.42(typescript@7.0.2))': dependencies: '@vueuse/core': 13.9.0(vue@3.5.42(typescript@7.0.2)) '@vueuse/shared': 13.9.0(vue@3.5.42(typescript@7.0.2)) vue: 3.5.42(typescript@7.0.2) optionalDependencies: - axios: 1.18.1(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2) + axios: 1.20.0(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2) focus-trap: 7.8.0 fuse.js: 7.5.0 nprogress: 0.2.0 @@ -20120,7 +20120,7 @@ snapshots: '@fastify/error': 4.2.0 fastq: 1.20.1 - axios@1.18.1(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2): + axios@1.20.0(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2): dependencies: follow-redirects: 1.16.0(debug@4.4.3(supports-color@10.2.2)) form-data: 4.0.6 @@ -21756,7 +21756,7 @@ snapshots: transitivePeerDependencies: - supports-color - fumadocs-openapi@12.0.1(2b987dc9753a22b34010c9167028a29c): + fumadocs-openapi@12.0.1(18308fb02f1370e08bd00c000219c030): dependencies: '@base-ui/react': 1.8.0(@date-fns/tz@1.5.0)(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@fuma-translate/react': 1.0.2(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -21780,7 +21780,7 @@ snapshots: shiki: 4.4.3 yaml: 2.9.1 optionalDependencies: - '@scalar/api-client-react': 2.0.51(axios@1.18.1(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2))(nprogress@0.2.0)(react@19.2.7)(supports-color@10.2.2)(tailwindcss@4.3.2)(typescript@7.0.2) + '@scalar/api-client-react': 2.0.51(axios@1.20.0(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2))(nprogress@0.2.0)(react@19.2.7)(supports-color@10.2.2)(tailwindcss@4.3.2)(typescript@7.0.2) '@types/react': 19.2.17 json-schema-typed: 8.0.2 transitivePeerDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index fe32a86b8..830d9d217 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -267,7 +267,7 @@ overrides: "assistant-cloud": 0.1.38 "@babel/helpers@<7.26.10": ">=7.29.7" "@babel/runtime@<7.26.10": ">=7.29.7" - "axios@>=1.0.0 <1.18.1": "1.18.1" + "axios@>=1.0.0 <1.18.1": "1.20.0" "brace-expansion@>=1.0.0 <1.1.18": "1.1.18" "brace-expansion@>=2.0.0 <2.1.4": "2.1.4" "brace-expansion@>=4.0.0 <5.0.9": "5.0.12" From 2778a1cb0f8c9acc05cffd303c27b9d3ef5fe8db Mon Sep 17 00:00:00 2001 From: "homarr-renovate[bot]" <158783068+homarr-renovate[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 20:10:43 +0000 Subject: [PATCH 06/29] chore(deps): update dependency ip-address@<10.3.1 to v10.7.1 [security] (#6964) Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 10 +++++----- pnpm-workspace.yaml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 37159e08f..e946d56d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -723,7 +723,7 @@ overrides: form-data@>=4.0.0 <4.0.4: '>=4.0.6' hono@<4.6.5: '>=4.13.7' immutable@>=5.0.0-beta.1 <5.1.8: 5.1.8 - ip-address@<10.3.1: 10.3.1 + ip-address@<10.3.1: 10.7.1 image-size@<=2.0.2: 2.0.3 js-yaml@>=3.0.0 <3.15.2: 3.15.2 js-yaml@>=4.0.0 <4.3.2: 4.3.2 @@ -10697,8 +10697,8 @@ packages: resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==} engines: {node: '>=12.22.0'} - ip-address@10.3.1: - resolution: {integrity: sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==} + ip-address@10.7.1: + resolution: {integrity: sha512-4OUAqU9Z1i3vCnS05hzGiFnEMDpQ+62pAD/MVQOp83fYyNC8GleCqaS0QikQBmcWCrKFiUs/B8ztRRiYOAXuCA==} engines: {node: '>= 12'} ipaddr.js@2.3.0: @@ -22341,7 +22341,7 @@ snapshots: transitivePeerDependencies: - supports-color - ip-address@10.3.1: {} + ip-address@10.7.1: {} ipaddr.js@2.3.0: {} @@ -25235,7 +25235,7 @@ snapshots: socks@2.8.3: dependencies: - ip-address: 10.3.1 + ip-address: 10.7.1 smart-buffer: 4.2.0 solid-js@1.9.14: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 830d9d217..5eaf70093 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -279,7 +279,7 @@ overrides: "form-data@>=4.0.0 <4.0.4": ">=4.0.6" "hono@<4.6.5": ">=4.13.7" "immutable@>=5.0.0-beta.1 <5.1.8": "5.1.8" - "ip-address@<10.3.1": "10.3.1" + "ip-address@<10.3.1": "10.7.1" "image-size@<=2.0.2": "2.0.3" "js-yaml@>=3.0.0 <3.15.2": "3.15.2" "js-yaml@>=4.0.0 <4.3.2": "4.3.2" From f46dbb630a02807ced921b8babe4f0fcdafa6f05 Mon Sep 17 00:00:00 2001 From: "homarr-renovate[bot]" <158783068+homarr-renovate[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 20:12:09 +0000 Subject: [PATCH 07/29] chore(deps): update dependency @grpc/grpc-js@<1.12.7 to v1.13.6 [security] (#6961) Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 12 ++++++------ pnpm-workspace.yaml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e946d56d2..e2a9f7491 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -705,7 +705,7 @@ overrides: '@assistant-ui/core': 0.3.7 '@assistant-ui/store': 0.3.5 '@assistant-ui/tap': 0.9.10 - '@grpc/grpc-js@<1.12.7': 1.12.7 + '@grpc/grpc-js@<1.12.7': 1.13.6 '@rvf/set-get@<7.0.2': 7.0.2 '@xmldom/xmldom@<0.8.15': 0.8.15 assistant-cloud: 0.1.38 @@ -4516,8 +4516,8 @@ packages: react: ^18.x || ^19.x react-dom: ^18.x || ^19.x - '@grpc/grpc-js@1.12.7': - resolution: {integrity: sha512-Tugep4V4GCsN9dSAVkXoBWqJXDoH+NSXd50kFGsm7ZG1tBXFEAnLoxzkGUIksTrDjiaMwlpsbC24oWbESGHpIQ==} + '@grpc/grpc-js@1.13.6': + resolution: {integrity: sha512-S9U8ioEds9fo8zCt4K8CyWHLJPg63FBgZo/hYUPVTiKppwsx/ykTnFg4g4/70beoa/DYao9uROQRcoahVH8PVQ==} engines: {node: '>=12.10.0'} '@grpc/proto-loader@0.7.13': @@ -15580,7 +15580,7 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@grpc/grpc-js@1.12.7': + '@grpc/grpc-js@1.13.6': dependencies: '@grpc/proto-loader': 0.7.13 '@js-sdsl/ordered-map': 4.4.2 @@ -21036,7 +21036,7 @@ snapshots: dockerode@5.0.0(supports-color@10.2.2): dependencies: '@balena/dockerignore': 1.0.2 - '@grpc/grpc-js': 1.12.7 + '@grpc/grpc-js': 1.13.6 '@grpc/proto-loader': 0.7.13 docker-modem: 5.0.7(supports-color@10.2.2) protobufjs: 7.6.6 @@ -21047,7 +21047,7 @@ snapshots: dockerode@5.0.1(supports-color@10.2.2): dependencies: '@balena/dockerignore': 1.0.2 - '@grpc/grpc-js': 1.12.7 + '@grpc/grpc-js': 1.13.6 '@grpc/proto-loader': 0.7.13 docker-modem: 5.0.7(supports-color@10.2.2) protobufjs: 7.6.6 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5eaf70093..aa7869d6a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -261,7 +261,7 @@ overrides: "@assistant-ui/core": 0.3.7 "@assistant-ui/store": 0.3.5 "@assistant-ui/tap": 0.9.10 - "@grpc/grpc-js@<1.12.7": "1.12.7" + "@grpc/grpc-js@<1.12.7": "1.13.6" "@rvf/set-get@<7.0.2": "7.0.2" "@xmldom/xmldom@<0.8.15": "0.8.15" "assistant-cloud": 0.1.38 From fb1537a24803280ca94c79a96577c14afe3c1a31 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 2 Oct 2026 20:15:37 +0000 Subject: [PATCH 08/29] docs(helm): update chart installation page --- .../docs/getting-started/installation/helm.md | 339 ++++++++++-------- 1 file changed, 181 insertions(+), 158 deletions(-) diff --git a/apps/docs/docs/getting-started/installation/helm.md b/apps/docs/docs/getting-started/installation/helm.md index 5e8058a9c..9092c0030 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.28.2](https://img.shields.io/badge/Version-8.28.2-informational?style=flat) +![Version: 8.30.0](https://img.shields.io/badge/Version-8.30.0-informational?style=flat) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat) -![AppVersion: v1.76.2](https://img.shields.io/badge/AppVersion-v1.76.2-informational?style=flat) +![AppVersion: v2.0.0](https://img.shields.io/badge/AppVersion-v2.0.0-informational?style=flat) A Helm chart to deploy homarr for Kubernetes @@ -12,7 +12,7 @@ A Helm chart to deploy homarr for Kubernetes ## Source Code -- +* ## Requirements @@ -76,39 +76,38 @@ To avoid including sensitive information in plain text within your version contr Below is an exhaustive list of all secrets: -| FEATURE | SECRET NAME | SECRET KEYS | Required | -| -------- | ---------------- | ------------------------------------ | -------- | -| OIDC | auth-oidc-secret | oidc-client-id
oidc-client-secret | No | -| LDAP | auth-ldap-secret | bind-password | No | -| DATABASE | db-secret | db-url | No | -| DATABASE | db-encryption | db-encryption-key | yes | +| FEATURE | SECRET NAME | SECRET KEYS | Required | +|-----------|-------------------------|-----------------------------------------|----------| +| OIDC | auth-oidc-secret | oidc-client-id
oidc-client-secret | No | +| LDAP | auth-ldap-secret | bind-password | No | +| DATABASE | db-secret | db-url | No | +| DATABASE | db-encryption | db-encryption-key | yes | ### Database -SQLite is the default; PostgreSQL is also supported. Existing MySQL installations must [convert to SQLite before upgrading to v2](/docs/advanced/mysql-to-sqlite). - You have multiple options for configuring the database: -| DRIVER TYPE | Persistence mode | -| -------------- | ---------------------------- | -| better-sqlite3 | Pod disk | -| better-sqlite3 | homarr-database PVC | -| node-postgres | External Postgresql database | +| DRIVER TYPE | Persistence mode | +|----------------|---------------------------------| +| better-sqlite3 | Pod disk | +| better-sqlite3 | homarr-database PVC | +| mysql2 | External MySql database | +| node-postgres | External Postgresql database | #### Pod disk -No additional configuration is required. However, keep in mind that if the pod restarts, all data will be lost. This setup is not _recommended_ for production use. +No additional configuration is required. However, keep in mind that if the pod restarts, all data will be lost. This setup is not *recommended* for production use. To create the necessary database secret, execute the following command:
Required Secrets -```yaml +````yaml kubectl create secret generic db-encryption \ --from-literal=db-encryption-key='' \ --namespace homarr -``` +````
@@ -121,11 +120,11 @@ Associated secret to create :
DB Required Secrets -```yaml +````yaml kubectl create secret generic db-encryption \ --from-literal=db-encryption-key='' \ --namespace homarr -``` +````
@@ -134,16 +133,47 @@ Bellow an example of the override value file :
values.yaml -```yaml +````yaml persistence: homarrDatabase: enabled: true storageClassName: "default" size: "1Gi" -``` +````
+#### External MySql database + +To create the necessary database secrets, execute the following command: + +
+Required Secrets + +````yaml +kubectl create secret generic db-encryption \ +--from-literal=db-encryption-key='' \ +--namespace homarr +```` + +````yaml +kubectl create secret generic db-secret \ +--from-literal=db-url='mysql://user:password@host:port/homarrdb' \ +--namespace homarr +```` +
+ +Below is an example of the override values file: + +
+values.yaml + +````yaml +database: + type: mysql +```` +
+ #### External Postgresql database To create the necessary database secrets, execute the following command: @@ -151,17 +181,17 @@ To create the necessary database secrets, execute the following command:
Required Secrets -```yaml +````yaml kubectl create secret generic db-encryption \ --from-literal=db-encryption-key='' \ --namespace homarr -``` +```` -```yaml +````yaml kubectl create secret generic db-secret \ --from-literal=db-url='postgresql://user:password@host:port/homarrdb' \ --namespace homarr -``` +````
@@ -170,11 +200,10 @@ Below is an example of the override values file:
values.yaml -```yaml +````yaml database: type: postgresql -``` - +````
### Ingress @@ -184,7 +213,7 @@ The ingress section in the values.yaml file allows you to configure how external
values.yaml -```yaml +````yaml service: enabled: true # Ensure the service is enabled for Ingress to route traffic ingress: @@ -202,8 +231,7 @@ ingress: - "homarr.homelab.dev" - "www.homarr.homelab.dev" secretName: homelab-tls -``` - +````
### HTTPRoute (Gateway API) @@ -214,9 +242,9 @@ This provides a more expressive and future-proof alternative to Ingress, with su
values.yaml -```yaml +````yaml service: - enabled: true # must be enabled for HTTPRoute to forward traffic + enabled: true # must be enabled for HTTPRoute to forward traffic httproute: enabled: true parentRefs: @@ -232,15 +260,14 @@ httproute: backendRefs: - name: homarr port: 7575 -``` - +````
#### Notes: - parentRefs: the Gateway(s) this route attaches to. Gateways must already exist. - hostnames: domain names this route applies to. -- rules: each rule can define matches (paths, headers, queries), optional filters, and backendRefs to services +- rules: each rule can define matches (paths, headers, queries), optional filters, and backendRefs to services - TLS must be configured on the Gateway (not on HTTPRoute). For example, the Gateway can have a TLS listener for homarr.homelab.dev, and this route will automatically apply once matched. ### Certificates @@ -257,7 +284,7 @@ Use inline certificates to generate a ConfigMap, mounted as individual files.
values.yaml -```yaml +````yaml persistence: homarrTrustedCerts: enabled: true @@ -271,8 +298,7 @@ persistence: -----BEGIN CERTIFICATE----- MIID...EFGH== -----END CERTIFICATE----- -``` - +````
Behavior: @@ -287,7 +313,7 @@ Use inline certificates to generate a Kubernetes Secret, mounted as files.
values.yaml -```yaml +````yaml persistence: homarrTrustedCerts: enabled: true @@ -301,8 +327,7 @@ persistence: -----BEGIN CERTIFICATE----- MIIC...XYZ== -----END CERTIFICATE----- -``` - +````
Behavior: @@ -317,17 +342,16 @@ Use an existing Kubernetes Secret, assuming its keys are filenames.
values.yaml -```yaml +````yaml persistence: homarrTrustedCerts: enabled: true type: existingSecret existingSecretName: "existingSecretName" existingSecretKeys: - - cert3.crt - - cert4.crt -``` - + - cert3.crt + - cert4.crt +````
Behavior: @@ -348,115 +372,114 @@ All available values are listed on the [artifacthub](https://artifacthub.io/pack ## Values -| Key | Type | Default | Description | -| -------------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| additionalObjects | list | `[]` | Additional resources to deploy. These objects are templated. | -| affinity | object | `{}` | Node affinity for pod scheduling | -| autoscaling.enabled | bool | `false` | Enable autoscaling | -| autoscaling.maxReplicas | int | `100` | Maximum replicas | -| autoscaling.minReplicas | int | `1` | Minimum replicas | -| autoscaling.targetCPUUtilizationPercentage | int | `80` | Target CPU utilization for autoscaling | -| containerPorts | object | `{"http":{"port":7575,"protocol":"TCP"}}` | containerPorts defines the ports to open on the container. It is a map where each entry specifies: - `port` (int) (required): The port number to expose inside the container. - `protocol` (string) (required): The network protocol (TCP or UDP) used for the port. - `disabled` (bool) : Optional flag to disable this port (defaults to false). Can be overridden via Helm values. By default, this configuration exposes TCP port 7575 with the name `http`. | -| database.migrationEnabled | bool | `true` | Database migration configuration. DB_MIGRATIONS_DISABLED Set to `true` to disable database migrations. Migrations are enabled by default (`false`). | -| database.type | string | `"sqlite"` | Database type: sqlite or postgresql | -| env.AUTH_COOKIE_PREFIX | string | `"homarr"` | Prefix used for all authentication cookies. Change if you run another Auth.js/NextAuth app on the same hostname to avoid cookie name collisions. Only letters, numbers, hyphens and underscores. | -| env.AUTH_LDAP_BASE | string | `nil` | Base dn of your LDAP server | -| env.AUTH_LDAP_BIND_DN | string | `nil` | User used for finding users and groups | -| env.AUTH_LDAP_GROUP_CLASS | string | `"groupOfUniqueNames"` | Class used for querying groups | -| env.AUTH_LDAP_GROUP_FILTER_EXTRA_ARG | string | `nil` | Extra arguments for user's groups search filter (& based) | -| env.AUTH_LDAP_GROUP_MEMBER_ATTRIBUTE | string | `"member"` | Attribute used for querying group member | -| env.AUTH_LDAP_GROUP_MEMBER_USER_ATTRIBUTE | string | `"dn"` | User attribute used for comparing with group member | -| env.AUTH_LDAP_SEARCH_SCOPE | string | `"base"` | LDAP search scope between base, one or sub | -| env.AUTH_LDAP_URI | string | `nil` | URI of your LDAP server | -| env.AUTH_LDAP_USERNAME_ATTRIBUTE | string | `"uid"` | Attribute used for username | -| env.AUTH_LDAP_USERNAME_FILTER_EXTRA_ARG | string | `nil` | Extra arguments for user search filter (& based) | -| env.AUTH_LDAP_USER_MAIL_ATTRIBUTE | string | `"mail"` | Attribute used for mail field | -| env.AUTH_LOGOUT_REDIRECT_URL | string | `nil` | URL to redirect to after clicking logging out. | -| env.AUTH_OIDC_AUTO_LOGIN | string | `"false"` | Automatically redirect to OIDC login | -| env.AUTH_OIDC_CLIENT_NAME | string | `"OIDC"` | Display name of provider (in login screen) | -| env.AUTH_OIDC_GROUPS_ATTRIBUTE | string | `"groups"` | Attribute used for groups (roles) claim | -| env.AUTH_OIDC_ISSUER | string | `nil` | Issuer URI of OIDC provider without trailing slash (/) | -| env.AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE | string | `nil` | Overwrite name attribute. By default, it will use preferred_username if it does not contain a @ and otherwise name. | -| env.AUTH_OIDC_SCOPE_OVERWRITE | string | `"openid email profile groups"` | Override the OIDC scopes | -| env.AUTH_PROVIDERS | string | `"credentials"` | Enabled authentication methods. Multiple providers can be enabled with by separating them with , (ex. AUTH_PROVIDERS=credentials,oidc, it is highly recommended to just enable one provider). | -| env.AUTH_SESSION_EXPIRY_TIME | string | `"30d"` | Time for the session to time out. Can be set as pure number, which will automatically be used in seconds, or followed by s, m, h or d for seconds, minutes, hours or days. (ex: "30m") | -| env.ENABLE_DNS_CACHING | string | `"false"` | Enables dns caching. This is not yet working for all users. See #4006 | -| env.LOG_LEVEL | string | `"info"` | Log level to use. Possible values: debug/info/warn/error | -| env.NO_EXTERNAL_CONNECTION | string | `"false"` | Disables some requests that need internet connection | -| env.TZ | string | `"Europe/Paris"` | Your local time zone | -| envSecrets.authLdapCredentials.existingSecret | string | `"auth-ldap-secret"` | Name of existing secret containing LDAP credentials | -| envSecrets.authLdapCredentials.ldapBindingPassword | string | `"bind-password"` | Password for bind user secret key | -| envSecrets.authOidcCredentials.existingSecret | string | `"auth-oidc-secret"` | Name of existing secret containing OIDC credentials | -| envSecrets.authOidcCredentials.oidcClientId | string | `"oidc-client-id"` | ID of OIDC client (application) secret key | -| envSecrets.authOidcCredentials.oidcClientSecret | string | `"oidc-client-secret"` | Secret of OIDC client (application) secret key | -| envSecrets.dbCredentials.dbUrlKey | string | `"db-url"` | Secret key for DB_URL Example for external database: `postgresql://username:password@host:port/homarrdb` | -| envSecrets.dbCredentials.existingSecret | string | `"db-secret"` | Name of existing secret containing DB credentials | -| envSecrets.dbEncryption.existingSecret | string | `"db-encryption"` | Name of existing secret containing DB encryption | -| envSecrets.dbEncryption.key | string | `"db-encryption-key"` | Secret key for SECRET_ENCRYPTION_KEY can be generated with `openssl rand -hex 32` | -| fullnameOverride | string | `""` | Overrides chart's fullname | -| hostAliases | list | `[]` | Add static entries to /etc/hosts in the Pod. This is useful in the following cases: - You are running in a dual-stack cluster (IPv4 + IPv6) and want to force usage of IPv4 for specific hostnames - Your application is having DNS resolution issues or IPv6 preference issues - You need to override or simulate DNS entries without changing global DNS - You are running in an air-gapped or isolated environment without external DNS Example: hostAliases: - ip: "192.168.1.10" hostnames: - "example.com" - "example.internal" | -| httproute | object | `{"enabled":false,"hostnames":["chart-example.local"],"parentRefs":[{"name":"my-gateway","namespace":"default"}],"rules":[{"backendRefs":[{"name":"homarr","port":7575}],"filters":[],"matches":[{"path":{"type":"PathPrefix","value":"/"}}]}]}` | Gateway API HTTPRoute configuration | -| httproute.enabled | bool | `false` | Enable HTTPRoute | -| httproute.hostnames | list | `["chart-example.local"]` | Hostnames this route matches (similar to ingress.hosts.host) | -| httproute.parentRefs | list | `[{"name":"my-gateway","namespace":"default"}]` | References to the parent Gateway(s) this route attaches to. Each item must include at least a `name`, and optionally a `namespace`. | -| httproute.rules | list | `[{"backendRefs":[{"name":"homarr","port":7575}],"filters":[],"matches":[{"path":{"type":"PathPrefix","value":"/"}}]}]` | List of routing rules. Each rule can include: - matches: path/header/query matching - filters: optional transformations (redirects, header modifications, etc.) - backendRefs: one or more Kubernetes Services to forward traffic to | -| httproute.rules[0].filters | list | `[]` | Optional filters for this rule (default: empty) | -| httproute.rules[0].matches[0].path.type | string | `"PathPrefix"` | Path match type. One of: Exact, PathPrefix, RegularExpression | -| 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.76.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 | -| ingress.hosts | list | `[{"host":"chart-example.local","paths":[{"path":"/"}]}]` | Ingress hosts configuration | -| ingress.ingressClassName | string | `""` | Ingress class name | -| ingress.tls | list | `[]` | Ingress TLS configuration | -| livenessProbe.failureThreshold | int | `3` | Failure threshold for liveness probe - number of consecutive failures before pod is restarted | -| livenessProbe.httpGet.path | string | `"/api/health/live"` | This is the liveness check endpoint used by Kubernetes to determine if the application is still running. | -| livenessProbe.httpGet.port | int | `7575` | The port on which the liveness check will be performed. This must be the same as the container port exposed by the application. | -| livenessProbe.initialDelaySeconds | int | `10` | Initial delay in seconds before the liveness probe starts | -| livenessProbe.periodSeconds | int | `10` | Period in seconds between liveness probe checks | -| livenessProbe.timeoutSeconds | int | `1` | Timeout in seconds for each liveness probe check | -| nameOverride | string | `""` | Overrides chart's name | -| nodeSelector | object | `{}` | Node selectors for pod scheduling | -| persistence.homarrDatabase.accessMode | string | `"ReadWriteOnce"` | homarr-database access mode | -| persistence.homarrDatabase.enabled | bool | `false` | Enable homarr-database persistent storage | -| persistence.homarrDatabase.mountPath | string | `"/appdata"` | homarr-database mount path inside the pod | -| persistence.homarrDatabase.name | string | `"homarr-database"` | homarr-database persistent storage name | -| persistence.homarrDatabase.size | string | `"50Mi"` | homarr-database storage size | -| persistence.homarrDatabase.storageClassName | string | `"local-path"` | homarr-database storage class name | -| persistence.homarrDatabase.volumeClaimName | string | `""` | homarr-database optional volumeClaimName to target specific PV | -| persistence.homarrTrustedCerts.certificates | string | `nil` | homarr-trusted-certificates certificates, each entry will become a new trusted certificate as a dedicated file (works only for "configmap" and "secret" mode) | -| persistence.homarrTrustedCerts.enabled | bool | `false` | Enable trusted certificates persistence | -| persistence.homarrTrustedCerts.existingSecretKeys | string | `nil` | List of keys (filenames) to mount from the existing secret (used only when type is "existingSecret") | -| persistence.homarrTrustedCerts.existingSecretName | string | `""` | Name of the existing Kubernetes Secret to mount (required if type is "existingSecret") | -| persistence.homarrTrustedCerts.mountPath | string | `"/appdata/trusted-certificates"` | homarr-trusted-certificates mount path inside the pod | -| persistence.homarrTrustedCerts.type | string | `"configmap"` | Persistence mode can be : configmap (declarative), secret (declarative) or existingSecret (mount an existing Kubernetes Secret by name and specify which keys to mount as files) | -| podAnnotations | object | `{}` | Pod annotations | -| podLabels | object | `{}` | Pod labels | -| podSecurityContext | object | `{}` | Pod security context | -| rbac | object | `{"enabled":false}` | Enable RBAC resources for Kubernetes integration Creates Role, ClusterRole, and associated bindings for Homarr's Kubernetes features | -| rbac.enabled | bool | `false` | Enable to create RBAC resources and activate Kubernetes integration | -| readinessProbe.failureThreshold | int | `3` | Failure threshold for readiness probe - number of consecutive failures before pod is considered unready | -| readinessProbe.httpGet.path | string | `"/api/health/ready"` | This is the readiness check endpoint used by Kubernetes to determine if the application is ready to handle traffic. | -| readinessProbe.httpGet.port | int | `7575` | The port on which the readiness check will be performed. This must match the container's exposed port. | -| readinessProbe.initialDelaySeconds | int | `10` | Initial delay in seconds before the readiness probe starts. increase this value if the pod is slow to fully start. | -| readinessProbe.periodSeconds | int | `10` | Period in seconds between readiness probe checks | -| readinessProbe.timeoutSeconds | int | `1` | Timeout in seconds for each readiness probe check | -| replicaCount | int | `1` | Number of replicas | -| resources | object | `{}` | Resource configuration | -| securityContext | object | `{}` | Security context | -| service.enabled | bool | `true` | Enable service | -| service.ipFamilies | list | `[]` | List of IP families to use for the service. Examples: - ["IPv4"] - ["IPv6"] - ["IPv4", "IPv6"] for dual-stack Leave empty to use cluster default behavior | -| service.ipFamilyPolicy | string | `"SingleStack"` | Defines how the service assigns IP families (IPv4/IPv6) Possible values: - SingleStack (default): Only one IP family, usually IPv4 - PreferDualStack: Use dual-stack if the cluster supports it, fallback to single - RequireDualStack: Fail if dual-stack cannot be assigned | -| service.ports.app.port | int | `7575` | Service port | -| service.ports.app.protocol | string | `"TCP"` | Service protocol | -| service.ports.app.targetPort | string | `"http"` | Service target port | -| service.type | string | `"ClusterIP"` | Service type | -| strategyType | string | `"RollingUpdate"` | `strategyType` specifies the strategy used to replace old Pods by new ones. `strategyType` can be `"Recreate"` or `"RollingUpdate"`. `"RollingUpdate"` is the default value and updates Pods in a rolling update fashion. `"Recreate"` will kill all existing Pods before new ones are created. The `"Recreate"` strategy is necessary when persistent volume's `accessMode` is set to `"ReadWriteOnce"` when using `helm upgrade`, as pod volume attachments to an existing PersistentVolumeClaim need to be cleared before a new pod can attach to it. | -| tolerations | list | `[]` | Node tolerations for pod scheduling | +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| additionalObjects | list | `[]` | Additional resources to deploy. These objects are templated. | +| affinity | object | `{}` | Node affinity for pod scheduling | +| autoscaling.enabled | bool | `false` | Enable autoscaling | +| autoscaling.maxReplicas | int | `100` | Maximum replicas | +| autoscaling.minReplicas | int | `1` | Minimum replicas | +| autoscaling.targetCPUUtilizationPercentage | int | `80` | Target CPU utilization for autoscaling | +| containerPorts | object | `{"http":{"port":7575,"protocol":"TCP"}}` | containerPorts defines the ports to open on the container. It is a map where each entry specifies: - `port` (int) (required): The port number to expose inside the container. - `protocol` (string) (required): The network protocol (TCP or UDP) used for the port. - `disabled` (bool) : Optional flag to disable this port (defaults to false). Can be overridden via Helm values. By default, this configuration exposes TCP port 7575 with the name `http`. | +| database.migrationEnabled | bool | `true` | Database migration configuration. DB_MIGRATIONS_DISABLED Set to `true` to disable database migrations. Migrations are enabled by default (`false`). | +| database.type | string | `"sqlite"` | Database type: sqlite, mysql or postgresql | +| env.AUTH_COOKIE_PREFIX | string | `"homarr"` | Prefix used for all authentication cookies. Change if you run another Auth.js/NextAuth app on the same hostname to avoid cookie name collisions. Only letters, numbers, hyphens and underscores. | +| env.AUTH_LDAP_BASE | string | `nil` | Base dn of your LDAP server | +| env.AUTH_LDAP_BIND_DN | string | `nil` | User used for finding users and groups | +| env.AUTH_LDAP_GROUP_CLASS | string | `"groupOfUniqueNames"` | Class used for querying groups | +| env.AUTH_LDAP_GROUP_FILTER_EXTRA_ARG | string | `nil` | Extra arguments for user's groups search filter (& based) | +| env.AUTH_LDAP_GROUP_MEMBER_ATTRIBUTE | string | `"member"` | Attribute used for querying group member | +| env.AUTH_LDAP_GROUP_MEMBER_USER_ATTRIBUTE | string | `"dn"` | User attribute used for comparing with group member | +| env.AUTH_LDAP_SEARCH_SCOPE | string | `"base"` | LDAP search scope between base, one or sub | +| env.AUTH_LDAP_URI | string | `nil` | URI of your LDAP server | +| env.AUTH_LDAP_USERNAME_ATTRIBUTE | string | `"uid"` | Attribute used for username | +| env.AUTH_LDAP_USERNAME_FILTER_EXTRA_ARG | string | `nil` | Extra arguments for user search filter (& based) | +| env.AUTH_LDAP_USER_MAIL_ATTRIBUTE | string | `"mail"` | Attribute used for mail field | +| env.AUTH_LOGOUT_REDIRECT_URL | string | `nil` | URL to redirect to after clicking logging out. | +| env.AUTH_OIDC_AUTO_LOGIN | string | `"false"` | Automatically redirect to OIDC login | +| env.AUTH_OIDC_CLIENT_NAME | string | `"OIDC"` | Display name of provider (in login screen) | +| env.AUTH_OIDC_GROUPS_ATTRIBUTE | string | `"groups"` | Attribute used for groups (roles) claim | +| env.AUTH_OIDC_ISSUER | string | `nil` | Issuer URI of OIDC provider without trailing slash (/) | +| env.AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE | string | `nil` | Overwrite name attribute. By default, it will use preferred_username if it does not contain a @ and otherwise name. | +| env.AUTH_OIDC_SCOPE_OVERWRITE | string | `"openid email profile groups"` | Override the OIDC scopes | +| env.AUTH_PROVIDERS | string | `"credentials"` | Enabled authentication methods. Multiple providers can be enabled with by separating them with , (ex. AUTH_PROVIDERS=credentials,oidc, it is highly recommended to just enable one provider). | +| env.AUTH_SESSION_EXPIRY_TIME | string | `"30d"` | Time for the session to time out. Can be set as pure number, which will automatically be used in seconds, or followed by s, m, h or d for seconds, minutes, hours or days. (ex: "30m") | +| env.ENABLE_DNS_CACHING | string | `"false"` | Enables dns caching. This is not yet working for all users. See #4006 | +| env.LOG_LEVEL | string | `"info"` | Log level to use. Possible values: debug/info/warn/error | +| env.NO_EXTERNAL_CONNECTION | string | `"false"` | Disables some requests that need internet connection | +| env.TZ | string | `"Europe/Paris"` | Your local time zone | +| envSecrets.authLdapCredentials.existingSecret | string | `"auth-ldap-secret"` | Name of existing secret containing LDAP credentials | +| envSecrets.authLdapCredentials.ldapBindingPassword | string | `"bind-password"` | Password for bind user secret key | +| envSecrets.authOidcCredentials.existingSecret | string | `"auth-oidc-secret"` | Name of existing secret containing OIDC credentials | +| envSecrets.authOidcCredentials.oidcClientId | string | `"oidc-client-id"` | ID of OIDC client (application) secret key | +| envSecrets.authOidcCredentials.oidcClientSecret | string | `"oidc-client-secret"` | Secret of OIDC client (application) secret key | +| envSecrets.dbCredentials.dbUrlKey | string | `"db-url"` | Secret key for DB_URL Example for external database: `mysql://username:password@host:port/homarrdb` or `postgresql://username:password@host:port/homarrdb` | +| envSecrets.dbCredentials.existingSecret | string | `"db-secret"` | Name of existing secret containing DB credentials | +| envSecrets.dbEncryption.existingSecret | string | `"db-encryption"` | Name of existing secret containing DB encryption | +| envSecrets.dbEncryption.key | string | `"db-encryption-key"` | Secret key for SECRET_ENCRYPTION_KEY can be generated with `openssl rand -hex 32` | +| fullnameOverride | string | `""` | Overrides chart's fullname | +| hostAliases | list | `[]` | Add static entries to /etc/hosts in the Pod. This is useful in the following cases: - You are running in a dual-stack cluster (IPv4 + IPv6) and want to force usage of IPv4 for specific hostnames - Your application is having DNS resolution issues or IPv6 preference issues - You need to override or simulate DNS entries without changing global DNS - You are running in an air-gapped or isolated environment without external DNS Example: hostAliases: - ip: "192.168.1.10" hostnames: - "example.com" - "example.internal" | +| httproute | object | `{"enabled":false,"hostnames":["chart-example.local"],"parentRefs":[{"name":"my-gateway","namespace":"default"}],"rules":[{"backendRefs":[{"name":"homarr","port":7575}],"filters":[],"matches":[{"path":{"type":"PathPrefix","value":"/"}}]}]}` | Gateway API HTTPRoute configuration | +| httproute.enabled | bool | `false` | Enable HTTPRoute | +| httproute.hostnames | list | `["chart-example.local"]` | Hostnames this route matches (similar to ingress.hosts.host) | +| httproute.parentRefs | list | `[{"name":"my-gateway","namespace":"default"}]` | References to the parent Gateway(s) this route attaches to. Each item must include at least a `name`, and optionally a `namespace`. | +| httproute.rules | list | `[{"backendRefs":[{"name":"homarr","port":7575}],"filters":[],"matches":[{"path":{"type":"PathPrefix","value":"/"}}]}]` | List of routing rules. Each rule can include: - matches: path/header/query matching - filters: optional transformations (redirects, header modifications, etc.) - backendRefs: one or more Kubernetes Services to forward traffic to | +| httproute.rules[0].filters | list | `[]` | Optional filters for this rule (default: empty) | +| httproute.rules[0].matches[0].path.type | string | `"PathPrefix"` | Path match type. One of: Exact, PathPrefix, RegularExpression | +| 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 | `"v2.0.0"` | 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 | +| ingress.hosts | list | `[{"host":"chart-example.local","paths":[{"path":"/"}]}]` | Ingress hosts configuration | +| ingress.ingressClassName | string | `""` | Ingress class name | +| ingress.tls | list | `[]` | Ingress TLS configuration | +| livenessProbe.failureThreshold | int | `3` | Failure threshold for liveness probe - number of consecutive failures before pod is restarted | +| livenessProbe.httpGet.path | string | `"/api/health/live"` | This is the liveness check endpoint used by Kubernetes to determine if the application is still running. | +| livenessProbe.httpGet.port | int | `7575` | The port on which the liveness check will be performed. This must be the same as the container port exposed by the application. | +| livenessProbe.initialDelaySeconds | int | `10` | Initial delay in seconds before the liveness probe starts | +| livenessProbe.periodSeconds | int | `10` | Period in seconds between liveness probe checks | +| livenessProbe.timeoutSeconds | int | `1` | Timeout in seconds for each liveness probe check | +| nameOverride | string | `""` | Overrides chart's name | +| nodeSelector | object | `{}` | Node selectors for pod scheduling | +| persistence.homarrDatabase.accessMode | string | `"ReadWriteOnce"` | homarr-database access mode | +| persistence.homarrDatabase.enabled | bool | `false` | Enable homarr-database persistent storage | +| persistence.homarrDatabase.mountPath | string | `"/appdata"` | homarr-database mount path inside the pod | +| persistence.homarrDatabase.name | string | `"homarr-database"` | homarr-database persistent storage name | +| persistence.homarrDatabase.size | string | `"50Mi"` | homarr-database storage size | +| persistence.homarrDatabase.storageClassName | string | `"local-path"` | homarr-database storage class name | +| persistence.homarrDatabase.volumeClaimName | string | `""` | homarr-database optional volumeClaimName to target specific PV | +| persistence.homarrTrustedCerts.certificates | string | `nil` | homarr-trusted-certificates certificates, each entry will become a new trusted certificate as a dedicated file (works only for "configmap" and "secret" mode) | +| persistence.homarrTrustedCerts.enabled | bool | `false` | Enable trusted certificates persistence | +| persistence.homarrTrustedCerts.existingSecretKeys | string | `nil` | List of keys (filenames) to mount from the existing secret (used only when type is "existingSecret") | +| persistence.homarrTrustedCerts.existingSecretName | string | `""` | Name of the existing Kubernetes Secret to mount (required if type is "existingSecret") | +| persistence.homarrTrustedCerts.mountPath | string | `"/appdata/trusted-certificates"` | homarr-trusted-certificates mount path inside the pod | +| persistence.homarrTrustedCerts.type | string | `"configmap"` | Persistence mode can be : configmap (declarative), secret (declarative) or existingSecret (mount an existing Kubernetes Secret by name and specify which keys to mount as files) | +| podAnnotations | object | `{}` | Pod annotations | +| podLabels | object | `{}` | Pod labels | +| podSecurityContext | object | `{}` | Pod security context | +| rbac | object | `{"enabled":false}` | Enable RBAC resources for Kubernetes integration Creates Role, ClusterRole, and associated bindings for Homarr's Kubernetes features | +| rbac.enabled | bool | `false` | Enable to create RBAC resources and activate Kubernetes integration | +| readinessProbe.failureThreshold | int | `3` | Failure threshold for readiness probe - number of consecutive failures before pod is considered unready | +| readinessProbe.httpGet.path | string | `"/api/health/ready"` | This is the readiness check endpoint used by Kubernetes to determine if the application is ready to handle traffic. | +| readinessProbe.httpGet.port | int | `7575` | The port on which the readiness check will be performed. This must match the container's exposed port. | +| readinessProbe.initialDelaySeconds | int | `10` | Initial delay in seconds before the readiness probe starts. increase this value if the pod is slow to fully start. | +| readinessProbe.periodSeconds | int | `10` | Period in seconds between readiness probe checks | +| readinessProbe.timeoutSeconds | int | `1` | Timeout in seconds for each readiness probe check | +| replicaCount | int | `1` | Number of replicas | +| resources | object | `{}` | Resource configuration | +| securityContext | object | `{}` | Security context | +| service.enabled | bool | `true` | Enable service | +| service.ipFamilies | list | `[]` | List of IP families to use for the service. Examples: - ["IPv4"] - ["IPv6"] - ["IPv4", "IPv6"] for dual-stack Leave empty to use cluster default behavior | +| service.ipFamilyPolicy | string | `"SingleStack"` | Defines how the service assigns IP families (IPv4/IPv6) Possible values: - SingleStack (default): Only one IP family, usually IPv4 - PreferDualStack: Use dual-stack if the cluster supports it, fallback to single - RequireDualStack: Fail if dual-stack cannot be assigned | +| service.ports.app.port | int | `7575` | Service port | +| service.ports.app.protocol | string | `"TCP"` | Service protocol | +| service.ports.app.targetPort | string | `"http"` | Service target port | +| service.type | string | `"ClusterIP"` | Service type | +| strategyType | string | `"RollingUpdate"` | `strategyType` specifies the strategy used to replace old Pods by new ones. `strategyType` can be `"Recreate"` or `"RollingUpdate"`. `"RollingUpdate"` is the default value and updates Pods in a rolling update fashion. `"Recreate"` will kill all existing Pods before new ones are created. The `"Recreate"` strategy is necessary when persistent volume's `accessMode` is set to `"ReadWriteOnce"` when using `helm upgrade`, as pod volume attachments to an existing PersistentVolumeClaim need to be cleared before a new pod can attach to it. | +| tolerations | list | `[]` | Node tolerations for pod scheduling | --- - Autogenerated from chart metadata using [helm-docs](https://github.com/norwoodj/helm-docs) From 97474ebc8710f22f51a05bf34e59b850229cbbb3 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 3 Oct 2026 01:08:42 +0200 Subject: [PATCH 09/29] fix: restore app icon sizing on scaled boards (#6972) --- packages/widgets/src/app/app.module.css | 12 ++++++++ packages/widgets/src/app/component.tsx | 40 ++++++++++++++++++++----- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/packages/widgets/src/app/app.module.css b/packages/widgets/src/app/app.module.css index b14962396..f5535be88 100644 --- a/packages/widgets/src/app/app.module.css +++ b/packages/widgets/src/app/app.module.css @@ -1,4 +1,16 @@ +.appText { + min-height: 0; + min-width: 0; + overflow: hidden; +} + +.appText > * { + flex-shrink: 0; +} + .appIcon { + /* Keep labels and descriptions from consuming all of a short tile. */ + min-height: 40%; max-height: 100%; max-width: 100%; overflow: auto; diff --git a/packages/widgets/src/app/component.tsx b/packages/widgets/src/app/component.tsx index cb8c3cef6..87d48cd62 100644 --- a/packages/widgets/src/app/component.tsx +++ b/packages/widgets/src/app/component.tsx @@ -21,7 +21,13 @@ import classes from "./app.module.css"; import { PingDot } from "./ping/ping-dot"; import { PingIndicator } from "./ping/ping-indicator"; -export default function AppWidget({ options, isEditMode, height, width }: WidgetComponentProps<"app">) { +export default function AppWidget({ + options, + isEditMode, + height, + width, + displayScale = 1, +}: WidgetComponentProps<"app">) { const tCommon = useI18n("common"); const settings = useSettings(); const board = useRequiredBoard(); @@ -33,33 +39,50 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget if (isInitialWidgetQueryPending(appQuery)) return ; if (!app) return ; - const isTiny = height < 100 || width < 100; + let layoutScale = displayScale; + if (!Number.isFinite(layoutScale) || layoutScale <= 0) layoutScale = 1; + + // Widget dimensions are logical pixels; compact sizing follows the displayed tile. + const isTiny = height * layoutScale < 100 || width * layoutScale < 100; const isColumnLayout = options.layout.startsWith("column"); + let padding = 12; + let fontSize = rem(14); + let gap = 12; + let titleLineClamp = 2; + if (isTiny) { + padding = 4; + fontSize = rem(8); + titleLineClamp = 1; + } + if (isColumnLayout) gap = 0; return ( - + e.preventDefault() : undefined} > - + {options.showTitle && ( {app.name} @@ -68,7 +91,8 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget {options.descriptionDisplayMode === "normal" && ( Date: Sat, 3 Oct 2026 01:45:39 +0200 Subject: [PATCH 10/29] chore: update bug report template (#6957) --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 3e8e67c33..6b2e73482 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -45,6 +45,7 @@ body: options: # The below comment is used to insert a new version with on-release.yml #NEXT_VERSION# + - 2.0.0 - 1.77.0 - 1.76.2 - 1.76.1 From 819337afc6fbeee904f98c9a40807b86463085b5 Mon Sep 17 00:00:00 2001 From: "homarr-renovate[bot]" <158783068+homarr-renovate[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 23:52:12 +0000 Subject: [PATCH 11/29] chore(deps): update dependency brace-expansion@>=2.0.0 <2.1.4 to v2.1.7 [security] (#6967) Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 12 ++++++------ pnpm-workspace.yaml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e2a9f7491..54db927ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -713,7 +713,7 @@ overrides: '@babel/runtime@<7.26.10': '>=7.29.7' axios@>=1.0.0 <1.18.1: 1.20.0 brace-expansion@>=1.0.0 <1.1.18: 1.1.18 - brace-expansion@>=2.0.0 <2.1.4: 2.1.4 + brace-expansion@>=2.0.0 <2.1.4: 2.1.7 brace-expansion@>=4.0.0 <5.0.9: 5.0.12 browserslist@<4.28.7: 4.28.7 esbuild@<=0.24.2: '>=0.28.1' @@ -8866,8 +8866,8 @@ packages: brace-expansion@1.1.18: resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==} - brace-expansion@2.1.4: - resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} + brace-expansion@2.1.7: + resolution: {integrity: sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==} brace-expansion@5.0.12: resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} @@ -20229,7 +20229,7 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.1.4: + brace-expansion@2.1.7: dependencies: balanced-match: 1.0.2 @@ -23369,11 +23369,11 @@ snapshots: minimatch@5.1.9: dependencies: - brace-expansion: 2.1.4 + brace-expansion: 2.1.7 minimatch@9.0.9: dependencies: - brace-expansion: 2.1.4 + brace-expansion: 2.1.7 minimist@1.2.8: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index aa7869d6a..ebef78736 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -269,7 +269,7 @@ overrides: "@babel/runtime@<7.26.10": ">=7.29.7" "axios@>=1.0.0 <1.18.1": "1.20.0" "brace-expansion@>=1.0.0 <1.1.18": "1.1.18" - "brace-expansion@>=2.0.0 <2.1.4": "2.1.4" + "brace-expansion@>=2.0.0 <2.1.4": "2.1.7" "brace-expansion@>=4.0.0 <5.0.9": "5.0.12" "browserslist@<4.28.7": "4.28.7" "esbuild@<=0.24.2": ">=0.28.1" From 3adb63850d8fd8f9ea2b3d72db82f4591f950e41 Mon Sep 17 00:00:00 2001 From: "homarr-renovate[bot]" <158783068+homarr-renovate[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 23:54:34 +0000 Subject: [PATCH 12/29] chore(deps): update dependency next to v16.3.6 [security] (#6960) Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 158 ++++++++++++++++++++++---------------------- pnpm-workspace.yaml | 2 +- 2 files changed, 80 insertions(+), 80 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 54db927ca..3847f1666 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -529,8 +529,8 @@ catalogs: specifier: ^11.16.1 version: 11.17.2 next: - specifier: 16.3.3 - version: 16.3.3 + specifier: 16.3.6 + version: 16.3.6 next-auth: specifier: 5.0.0-beta.32 version: 5.0.0-beta.32 @@ -897,16 +897,16 @@ importers: version: 6.0.0 fumadocs-core: specifier: 'catalog:' - version: 16.15.12(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@modelcontextprotocol/server@2.0.0)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(algoliasearch@5.53.0)(lucide-react@1.47.0(react@19.2.7))(next@16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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)(supports-color@10.2.2)(zod@4.4.3) + version: 16.15.12(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@modelcontextprotocol/server@2.0.0)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(algoliasearch@5.53.0)(lucide-react@1.47.0(react@19.2.7))(next@16.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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)(supports-color@10.2.2)(zod@4.4.3) fumadocs-mdx: specifier: 'catalog:' - version: 15.4.3(ca407bc45757862c0553f46d1969d5d6) + version: 15.4.3(0bc99cc3ab6490dd33b76f4900ace488) fumadocs-openapi: specifier: 'catalog:' - version: 12.0.1(18308fb02f1370e08bd00c000219c030) + version: 12.0.1(226091f6acf12b47d5f750357219f3e3) fumadocs-ui: specifier: 'catalog:' - version: 16.15.12(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.15.12(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@modelcontextprotocol/server@2.0.0)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(algoliasearch@5.53.0)(lucide-react@1.47.0(react@19.2.7))(next@16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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)(supports-color@10.2.2)(zod@4.4.3))(next@16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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)(tailwindcss@4.3.2) + version: 16.15.12(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.15.12(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@modelcontextprotocol/server@2.0.0)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(algoliasearch@5.53.0)(lucide-react@1.47.0(react@19.2.7))(next@16.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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)(supports-color@10.2.2)(zod@4.4.3))(next@16.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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)(tailwindcss@4.3.2) json-schema-typed: specifier: 'catalog:' version: 8.0.2 @@ -915,7 +915,7 @@ importers: version: 11.17.2 next: specifier: 'catalog:' - version: 16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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-themes: specifier: 'catalog:' version: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -1186,7 +1186,7 @@ importers: 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.2(@tanstack/react-query@5.101.2(react@19.2.7))(next@16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(react@19.2.7) '@tanstack/react-query-persist-client': specifier: 'catalog:' version: 5.101.2(@tanstack/react-query@5.101.2(react@19.2.7))(react@19.2.7) @@ -1195,7 +1195,7 @@ importers: version: 11.18.0(@trpc/server@11.18.0(typescript@7.0.2))(typescript@7.0.2) '@trpc/next': specifier: 'catalog:' - 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@7.0.2))(typescript@7.0.2))(@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@7.0.2))(typescript@7.0.2))(@trpc/server@11.18.0(typescript@7.0.2))(react@19.2.7)(typescript@7.0.2))(@trpc/server@11.18.0(typescript@7.0.2))(next@16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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@7.0.2) + 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@7.0.2))(typescript@7.0.2))(@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@7.0.2))(typescript@7.0.2))(@trpc/server@11.18.0(typescript@7.0.2))(react@19.2.7)(typescript@7.0.2))(@trpc/server@11.18.0(typescript@7.0.2))(next@16.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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@7.0.2) '@trpc/react-query': specifier: 'catalog:' 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@7.0.2))(typescript@7.0.2))(@trpc/server@11.18.0(typescript@7.0.2))(react@19.2.7)(typescript@7.0.2) @@ -1267,7 +1267,7 @@ importers: version: 11.17.2 next: specifier: 'catalog:' - version: 16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.23) @@ -1595,7 +1595,7 @@ importers: version: 4.5.0 next: specifier: 'catalog:' - version: 16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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 @@ -1656,10 +1656,10 @@ importers: version: 8.1.8 next: specifier: 'catalog:' - version: 16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.32(next@16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.32(next@16.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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 @@ -1755,7 +1755,7 @@ importers: version: 1.11.21 next: specifier: 'catalog:' - version: 16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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 @@ -2424,7 +2424,7 @@ importers: version: 1.11.21 next: specifier: 'catalog:' - version: 16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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 @@ -2662,7 +2662,7 @@ importers: version: 9.6.0(@mantine/core@9.6.0(@mantine/hooks@9.6.0(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.6.0(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.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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 @@ -2729,7 +2729,7 @@ importers: version: 2.20.1(@babel/core@7.29.0(supports-color@10.2.2))(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7) next: specifier: 'catalog:' - version: 16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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 @@ -2766,10 +2766,10 @@ importers: version: mantine-react-table-open@9.0.3(@mantine/core@9.6.0(@mantine/hooks@9.6.0(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.6.0(@mantine/core@9.6.0(@mantine/hooks@9.6.0(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.6.0(react@19.2.7))(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.6.0(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.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.1(@swc/helpers@0.5.23)(next@16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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@7.0.2) + version: 4.13.1(@swc/helpers@0.5.23)(next@16.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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@7.0.2) react: specifier: 'catalog:' version: 19.2.7 @@ -2821,7 +2821,7 @@ importers: version: mantine-react-table-open@9.0.3(@mantine/core@9.6.0(@mantine/hooks@9.6.0(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.6.0(@mantine/core@9.6.0(@mantine/hooks@9.6.0(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.6.0(react@19.2.7))(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.6.0(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.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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 @@ -3045,7 +3045,7 @@ importers: version: mantine-react-table-open@9.0.3(@mantine/core@9.6.0(@mantine/hooks@9.6.0(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.6.0(@mantine/core@9.6.0(@mantine/hooks@9.6.0(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.6.0(react@19.2.7))(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.6.0(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.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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 @@ -5306,57 +5306,57 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@next/env@16.3.3': - resolution: {integrity: sha512-U2eYQRwXj+dsqxV79zFqExDdatnNY/ZWc2nsJU1p/OgT7fd3dXwlF6OjYaFQCfMoeTA19PWq+wVmYgimVA+V+g==} + '@next/env@16.3.6': + resolution: {integrity: sha512-x9Vblze1EbtltQYnNH38xCPWU3TVfBd1eXqA3+w9+BTpedkkdNpAaltXlGQ/nsc1+E0mVTNrtcbX3GoO09zeLQ==} - '@next/swc-darwin-arm64@16.3.3': - resolution: {integrity: sha512-8Hiv32QJPwdV6KYJ8meR9SBA061tQqnIKTJDocvOXlEQqib0xMFpzArosuffFUUc0sslbh7QQ8a3Yey1QV8EIw==} + '@next/swc-darwin-arm64@16.3.6': + resolution: {integrity: sha512-E/7GEqaUkt8mk/T8v9lAnrhzR06kdq1ZBkC12F8tAMkdIadwNp3H1KqHynDHrpcTlGCUdq/qu6vUL2aYVyYBdw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.3.3': - resolution: {integrity: sha512-A1lgKgwVchRYmSe467zdwhxT9040dd8lH+o65sL5Jet8fjB4kegw/rDyPIpYVRb6jAqwXFOJpjIXJLxQKLiE3A==} + '@next/swc-darwin-x64@16.3.6': + resolution: {integrity: sha512-yBE893/nDWTlaiBD1p+qgt7NUen4U5R6FXyH0s67Npq1S3E0cVSef1WIXC2xBRgQvwAvJq6DnS6Y6PrY0cy4Ew==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.3.3': - resolution: {integrity: sha512-bf0FIssMFueU2dm7vQEWWxk0c8UjKTdW0yzuh0sQsD8pf1+KCLDdaqhYZNMYGmXwEOiHAUzgBKudovIlcvvBjg==} + '@next/swc-linux-arm64-gnu@16.3.6': + resolution: {integrity: sha512-KJDpjBqBPYlvkivmyrp+Qys6k/7ksbqGQvRVc6ZEGfR+cjQxx+nUkJaWmNZJsmoOrqYNbaXByF8wa0lBwDhB3Q==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@16.3.3': - resolution: {integrity: sha512-W7viwCk9JY/cAkdz/A273rd5bb3RgT/IHwR7Upv90tunjBWNtAAhGhoecHh+teRNRSinuAFmE+l7fwZ4YKkrXg==} + '@next/swc-linux-arm64-musl@16.3.6': + resolution: {integrity: sha512-mqNg2K+hvWskSRb/QM+Ix412DvBsuSF0XV+frTSw5vmoucNnIlynFwKYew8D01bfATErMOM7Bujrf0BA5DRKFA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@16.3.3': - resolution: {integrity: sha512-0W46zw1N3ODpI6n0GeivHvvob1pooozgZVqy65k0mh4/7vr+FbY9+WpHzNVXjHipJf/A3FDheBG19H1s5A25rA==} + '@next/swc-linux-x64-gnu@16.3.6': + resolution: {integrity: sha512-nFncBNGAYouRHjRVaITs9beZRfhX4ssVwpnvPIAbkZVH6LtGoAVlH4bJ8Cnf9SOo9bsXgPFer/GdHtEE3JNOkw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@16.3.3': - resolution: {integrity: sha512-H4mBso8ZTMBPtdT0PN0pBx2ayTvQuTuvS6qT13d77yVFJXAPCxkyIhLTmdMaGTJs0krQYI/qpzdHijCeihXhbg==} + '@next/swc-linux-x64-musl@16.3.6': + resolution: {integrity: sha512-5Mf3cHDGR/Iz0ng2Bj3zUR3p5QS9YK3Hn2QiAfavFmyF48zwThAjpFoiTKNIcOHLYS4zEk+gzyJ/9deQ2ZB8yQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@16.3.3': - resolution: {integrity: sha512-cTMUJpcEGmeywofCUfhR+rSsoE33+rVPnPEYNTNdLNlsOeEg/vktOsKUSTb28vUGqD2jkm4Zaskcwn7OCI6FQg==} + '@next/swc-win32-arm64-msvc@16.3.6': + resolution: {integrity: sha512-0jkJy0C2kbrJWTk4YLa3xk80pVBpx8FCHJym7CnUfDAXe/FWv5qT7SQJbR0KuemyxaEDlEx5WT4VQJoTW+/9Qw==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.3.3': - resolution: {integrity: sha512-2VR4cTBzHXaBjnGsuH6GyJjENzQOmHeAh11uY1iUhjm3j5dEUrVJuUj+VL78jaGi/Dik8xS76zEj18BsFhlVZQ==} + '@next/swc-win32-x64-msvc@16.3.6': + resolution: {integrity: sha512-/YXjI1e5OXcZ7YpxRwgP/1jAV/SBKTzeVKqN2mk7mLpcICsyn3Gl5+dIfDTJp70M0ccMhyMMRso4v6mPDCGepg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -11653,8 +11653,8 @@ packages: '@types/unist': optional: true - next@16.3.3: - resolution: {integrity: sha512-tuRTx1nQ/yVw83cwJBo9F+njGUgMn3UHQycreWHB8XsStvvAh1AthbI8/4IpKnFaF58F+iSiHejYOlMQ/eq83g==} + next@16.3.6: + resolution: {integrity: sha512-L+otWM/aQbYTx98aZhgEoMb4bZAXx1YVW4UMA/vuCyCoWG5HJyZUili8QAkqzrcC+5///tsz3s0M+SlyB5bLMw==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -16442,30 +16442,30 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true - '@next/env@16.3.3': {} + '@next/env@16.3.6': {} - '@next/swc-darwin-arm64@16.3.3': + '@next/swc-darwin-arm64@16.3.6': optional: true - '@next/swc-darwin-x64@16.3.3': + '@next/swc-darwin-x64@16.3.6': optional: true - '@next/swc-linux-arm64-gnu@16.3.3': + '@next/swc-linux-arm64-gnu@16.3.6': optional: true - '@next/swc-linux-arm64-musl@16.3.3': + '@next/swc-linux-arm64-musl@16.3.6': optional: true - '@next/swc-linux-x64-gnu@16.3.3': + '@next/swc-linux-x64-gnu@16.3.6': optional: true - '@next/swc-linux-x64-musl@16.3.3': + '@next/swc-linux-x64-musl@16.3.6': optional: true - '@next/swc-win32-arm64-msvc@16.3.3': + '@next/swc-win32-arm64-msvc@16.3.6': optional: true - '@next/swc-win32-x64-msvc@16.3.3': + '@next/swc-win32-x64-msvc@16.3.6': optional: true '@nivo/annotations@0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': @@ -18790,10 +18790,10 @@ snapshots: '@tanstack/react-query': 5.101.2(react@19.2.7) react: 19.2.7 - '@tanstack/react-query-next-experimental@5.101.2(@tanstack/react-query@5.101.2(react@19.2.7))(next@16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.2(react@19.2.7) - next: 16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.2(@tanstack/react-query@5.101.2(react@19.2.7))(react@19.2.7)': @@ -19086,11 +19086,11 @@ snapshots: '@trpc/server': 11.18.0(typescript@7.0.2) typescript: 7.0.2 - '@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@7.0.2))(typescript@7.0.2))(@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@7.0.2))(typescript@7.0.2))(@trpc/server@11.18.0(typescript@7.0.2))(react@19.2.7)(typescript@7.0.2))(@trpc/server@11.18.0(typescript@7.0.2))(next@16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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@7.0.2)': + '@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@7.0.2))(typescript@7.0.2))(@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@7.0.2))(typescript@7.0.2))(@trpc/server@11.18.0(typescript@7.0.2))(react@19.2.7)(typescript@7.0.2))(@trpc/server@11.18.0(typescript@7.0.2))(next@16.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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@7.0.2)': dependencies: '@trpc/client': 11.18.0(@trpc/server@11.18.0(typescript@7.0.2))(typescript@7.0.2) '@trpc/server': 11.18.0(typescript@7.0.2) - next: 16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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: 7.0.2 @@ -21685,7 +21685,7 @@ snapshots: fsevents@2.3.3: optional: true - fumadocs-core@16.15.12(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@modelcontextprotocol/server@2.0.0)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(algoliasearch@5.53.0)(lucide-react@1.47.0(react@19.2.7))(next@16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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)(supports-color@10.2.2)(zod@4.4.3): + fumadocs-core@16.15.12(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@modelcontextprotocol/server@2.0.0)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(algoliasearch@5.53.0)(lucide-react@1.47.0(react@19.2.7))(next@16.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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)(supports-color@10.2.2)(zod@4.4.3): dependencies: '@fumari/image-size': 0.1.1 estree-util-value-to-estree: 3.5.0 @@ -21715,21 +21715,21 @@ snapshots: '@types/react': 19.2.17 algoliasearch: 5.53.0 lucide-react: 1.47.0(react@19.2.7) - next: 16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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) zod: 4.4.3 transitivePeerDependencies: - supports-color - fumadocs-mdx@15.4.3(ca407bc45757862c0553f46d1969d5d6): + fumadocs-mdx@15.4.3(0bc99cc3ab6490dd33b76f4900ace488): dependencies: '@mdx-js/mdx': 3.1.1(supports-color@10.2.2) '@standard-schema/spec': 1.1.0 chokidar: 5.0.0 esbuild: 0.28.2 estree-util-value-to-estree: 3.5.0 - fumadocs-core: 16.15.12(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@modelcontextprotocol/server@2.0.0)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(algoliasearch@5.53.0)(lucide-react@1.47.0(react@19.2.7))(next@16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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)(supports-color@10.2.2)(zod@4.4.3) + fumadocs-core: 16.15.12(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@modelcontextprotocol/server@2.0.0)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(algoliasearch@5.53.0)(lucide-react@1.47.0(react@19.2.7))(next@16.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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)(supports-color@10.2.2)(zod@4.4.3) github-slugger: 2.0.0 magic-string: 1.4.1 mdast-util-mdx: 3.0.0(supports-color@10.2.2) @@ -21749,14 +21749,14 @@ snapshots: '@types/mdx': 2.0.14 '@types/react': 19.2.17 mdast-util-directive: 3.1.0(supports-color@10.2.2) - next: 16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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 rolldown: 1.1.4 vite: 8.1.4(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.23))(terser@5.46.1)(tsx@4.20.4)(yaml@2.9.1) transitivePeerDependencies: - supports-color - fumadocs-openapi@12.0.1(18308fb02f1370e08bd00c000219c030): + fumadocs-openapi@12.0.1(226091f6acf12b47d5f750357219f3e3): dependencies: '@base-ui/react': 1.8.0(@date-fns/tz@1.5.0)(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@fuma-translate/react': 1.0.2(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -21768,8 +21768,8 @@ snapshots: chokidar: 5.0.0 class-variance-authority: 0.7.1 cn: 0.3.2 - fumadocs-core: 16.15.12(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@modelcontextprotocol/server@2.0.0)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(algoliasearch@5.53.0)(lucide-react@1.47.0(react@19.2.7))(next@16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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)(supports-color@10.2.2)(zod@4.4.3) - fumadocs-ui: 16.15.12(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.15.12(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@modelcontextprotocol/server@2.0.0)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(algoliasearch@5.53.0)(lucide-react@1.47.0(react@19.2.7))(next@16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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)(supports-color@10.2.2)(zod@4.4.3))(next@16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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)(tailwindcss@4.3.2) + fumadocs-core: 16.15.12(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@modelcontextprotocol/server@2.0.0)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(algoliasearch@5.53.0)(lucide-react@1.47.0(react@19.2.7))(next@16.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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)(supports-color@10.2.2)(zod@4.4.3) + fumadocs-ui: 16.15.12(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.15.12(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@modelcontextprotocol/server@2.0.0)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(algoliasearch@5.53.0)(lucide-react@1.47.0(react@19.2.7))(next@16.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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)(supports-color@10.2.2)(zod@4.4.3))(next@16.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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)(tailwindcss@4.3.2) github-slugger: 2.0.0 hast-util-to-jsx-runtime: 2.3.6(supports-color@10.2.2) lucide-react: 1.47.0(react@19.2.7) @@ -21788,7 +21788,7 @@ snapshots: - date-fns - supports-color - fumadocs-ui@16.15.12(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.15.12(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@modelcontextprotocol/server@2.0.0)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(algoliasearch@5.53.0)(lucide-react@1.47.0(react@19.2.7))(next@16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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)(supports-color@10.2.2)(zod@4.4.3))(next@16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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)(tailwindcss@4.3.2): + fumadocs-ui@16.15.12(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.15.12(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@modelcontextprotocol/server@2.0.0)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(algoliasearch@5.53.0)(lucide-react@1.47.0(react@19.2.7))(next@16.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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)(supports-color@10.2.2)(zod@4.4.3))(next@16.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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)(tailwindcss@4.3.2): dependencies: '@fuma-translate/react': 1.0.2(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@fumadocs/tailwind': 0.1.2(tailwindcss@4.3.2) @@ -21804,7 +21804,7 @@ snapshots: '@radix-ui/react-tabs': 1.1.21(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) class-variance-authority: 0.7.1 cn: 0.3.2 - fumadocs-core: 16.15.12(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@modelcontextprotocol/server@2.0.0)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(algoliasearch@5.53.0)(lucide-react@1.47.0(react@19.2.7))(next@16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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)(supports-color@10.2.2)(zod@4.4.3) + fumadocs-core: 16.15.12(@mdx-js/mdx@3.1.1(supports-color@10.2.2))(@modelcontextprotocol/server@2.0.0)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(algoliasearch@5.53.0)(lucide-react@1.47.0(react@19.2.7))(next@16.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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)(supports-color@10.2.2)(zod@4.4.3) lucide-react: 1.47.0(react@19.2.7) motion: 13.4.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next-themes: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -21818,7 +21818,7 @@ snapshots: optionalDependencies: '@types/mdx': 2.0.14 '@types/react': 19.2.17 - next: 16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) transitivePeerDependencies: - '@types/react-dom' - tailwindcss @@ -23473,22 +23473,22 @@ snapshots: neverpanic@0.0.8: {} - next-auth@5.0.0-beta.32(next@16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.32(next@16.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.3 - next: 16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.1: {} - next-intl@4.13.1(@swc/helpers@0.5.23)(next@16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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@7.0.2): + next-intl@4.13.1(@swc/helpers@0.5.23)(next@16.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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@7.0.2): dependencies: '@formatjs/intl-localematcher': 0.8.1 '@parcel/watcher': 2.4.1 '@swc/core': 1.15.3(@swc/helpers@0.5.23) icu-minify: 4.13.1 negotiator: 1.0.0 - next: 16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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 @@ -23519,9 +23519,9 @@ snapshots: transitivePeerDependencies: - supports-color - next@16.3.3(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.3.6(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(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.3.3 + '@next/env': 16.3.6 '@swc/helpers': 0.5.23 baseline-browser-mapping: 2.11.25 caniuse-lite: 1.0.30001810 @@ -23530,14 +23530,14 @@ snapshots: react-dom: 19.2.7(react@19.2.7) styled-jsx: 5.1.6(@babel/core@7.29.0(supports-color@10.2.2))(react@19.2.7) optionalDependencies: - '@next/swc-darwin-arm64': 16.3.3 - '@next/swc-darwin-x64': 16.3.3 - '@next/swc-linux-arm64-gnu': 16.3.3 - '@next/swc-linux-arm64-musl': 16.3.3 - '@next/swc-linux-x64-gnu': 16.3.3 - '@next/swc-linux-x64-musl': 16.3.3 - '@next/swc-win32-arm64-msvc': 16.3.3 - '@next/swc-win32-x64-msvc': 16.3.3 + '@next/swc-darwin-arm64': 16.3.6 + '@next/swc-darwin-x64': 16.3.6 + '@next/swc-linux-arm64-gnu': 16.3.6 + '@next/swc-linux-arm64-musl': 16.3.6 + '@next/swc-linux-x64-gnu': 16.3.6 + '@next/swc-linux-x64-musl': 16.3.6 + '@next/swc-win32-arm64-msvc': 16.3.6 + '@next/swc-win32-x64-msvc': 16.3.6 '@opentelemetry/api': 1.9.0 '@playwright/test': 1.61.1 babel-plugin-react-compiler: 1.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ebef78736..ed57ba836 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -177,7 +177,7 @@ catalog: mermaid: ^11.16.1 motion: ^13.0.0 maria2: ^0.4.1 - next: 16.3.3 + next: 16.3.6 next-auth: 5.0.0-beta.32 next-intl: 4.13.1 next-themes: ^0.4.6 From 25120cd2bebf52e42d911eacadf630a059fc8af7 Mon Sep 17 00:00:00 2001 From: "homarr-renovate[bot]" <158783068+homarr-renovate[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 04:17:29 +0000 Subject: [PATCH 13/29] chore(deps): update dependency brace-expansion@>=1.0.0 <1.1.18 to v1.1.21 [security] (#6966) Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 10 +++++----- pnpm-workspace.yaml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3847f1666..3e9eeade1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -712,7 +712,7 @@ overrides: '@babel/helpers@<7.26.10': '>=7.29.7' '@babel/runtime@<7.26.10': '>=7.29.7' axios@>=1.0.0 <1.18.1: 1.20.0 - brace-expansion@>=1.0.0 <1.1.18: 1.1.18 + brace-expansion@>=1.0.0 <1.1.18: 1.1.21 brace-expansion@>=2.0.0 <2.1.4: 2.1.7 brace-expansion@>=4.0.0 <5.0.9: 5.0.12 browserslist@<4.28.7: 4.28.7 @@ -8863,8 +8863,8 @@ packages: resolution: {integrity: sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==} engines: {node: '>=14.16'} - brace-expansion@1.1.18: - resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==} + brace-expansion@1.1.21: + resolution: {integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==} brace-expansion@2.1.7: resolution: {integrity: sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==} @@ -20224,7 +20224,7 @@ snapshots: widest-line: 4.0.1 wrap-ansi: 8.1.0 - brace-expansion@1.1.18: + brace-expansion@1.1.21: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 @@ -23365,7 +23365,7 @@ snapshots: minimatch@3.1.5: dependencies: - brace-expansion: 1.1.18 + brace-expansion: 1.1.21 minimatch@5.1.9: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ed57ba836..81b03ba3b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -268,7 +268,7 @@ overrides: "@babel/helpers@<7.26.10": ">=7.29.7" "@babel/runtime@<7.26.10": ">=7.29.7" "axios@>=1.0.0 <1.18.1": "1.20.0" - "brace-expansion@>=1.0.0 <1.1.18": "1.1.18" + "brace-expansion@>=1.0.0 <1.1.18": "1.1.21" "brace-expansion@>=2.0.0 <2.1.4": "2.1.7" "brace-expansion@>=4.0.0 <5.0.9": "5.0.12" "browserslist@<4.28.7": "4.28.7" From 907b166f46cd1929d6d2bcf7905e9428f831a37d Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 3 Oct 2026 11:25:41 +0200 Subject: [PATCH 14/29] docs(readme): fix installation links and sync integrations (#6996) --- docs/README.md | 275 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 253 insertions(+), 22 deletions(-) diff --git a/docs/README.md b/docs/README.md index eaa9dfee6..87553cede 100644 --- a/docs/README.md +++ b/docs/README.md @@ -22,7 +22,7 @@ Live Demo 🎮 • - + Install 💻 • @@ -95,20 +95,27 @@ + + Autobrr +
+

Autobrr

+
+ + Bazarr

Bazarr

- - + + Beszel

Beszel

- - + + Bindery
@@ -116,6 +123,20 @@
+ + Caddy +
+

Caddy

+
+ + + + Changedetection.io +
+

Changedetection.io

+
+ + Coolify
@@ -135,8 +156,8 @@

Deluge

- - + + Emby
@@ -144,6 +165,27 @@
+ + FileFlows +
+

FileFlows

+
+ + + + Frigate +
+

Frigate

+
+ + + + Gatus +
+

Gatus

+
+ + Glances
@@ -156,13 +198,20 @@

Gluetun

- - + + Gotify

Gotify

+ + + + Healthchecks +
+

Healthchecks

+
@@ -172,6 +221,13 @@ + + Homebox +
+

Homebox

+
+ + iCal
@@ -186,13 +242,20 @@
+ + Jackett +
+

Jackett

+
+ + Jellyfin

Jellyfin

- - + + Jellyseerr
@@ -200,13 +263,69 @@
+ + Jellystat +
+

Jellystat

+
+ + + + Karakeep +
+

Karakeep

+
+ + + + Komga +
+

Komga

+
+ + Lidarr

Lidarr

+ + + + Linkwarden +
+

Linkwarden

+
+ + + + llama.cpp +
+

llama.cpp

+
+ + Maintainerr +
+

Maintainerr

+
+ + + + Mealie +
+

Mealie

+
+ + + + Miniflux +
+

Miniflux

+
+ + Navidrome
@@ -214,13 +333,27 @@
+ + NetAlertX +
+

NetAlertX

+
+ + + + Netdata +
+

Netdata

+
+ + Nextcloud

Nextcloud

- - + + ntfy
@@ -254,8 +387,8 @@

Overseerr

- - + + Paperless-ngx
@@ -268,8 +401,8 @@

PatchMon

- - + + PeaNUT
@@ -284,6 +417,13 @@
+ + Plant-it +
+

Plant-it

+
+ + Plex
@@ -291,6 +431,13 @@
+ + Prometheus +
+

Prometheus

+
+ + Prowlarr
@@ -326,6 +473,13 @@
+ + RomM +
+

RomM

+
+ + SABnzbd
@@ -333,13 +487,20 @@
+ + Scrutiny +
+

Scrutiny

+
+ + Seerr

Seerr

- - + + Slskd
@@ -352,8 +513,8 @@

Sonarr

- - + + Speedtest Tracker
@@ -361,11 +522,39 @@
+ + Spoolman +
+

Spoolman

+
+ + + + Stash +
+

Stash

+
+ + + + Syncthing Relay +
+

Syncthing
Relay

+
+ + Synology DiskStation

Synology
DiskStation

+ + + + Tandoor +
+

Tandoor

+
@@ -401,6 +590,13 @@

Transmission

+ + + + Trilium +
+

Trilium

+
@@ -410,6 +606,13 @@ + + Tube Archivist +
+

Tube
Archivist

+
+ + Umami
@@ -424,6 +627,13 @@
+ + Unmanic +
+

Unmanic

+
+ + Unraid
@@ -436,6 +646,13 @@

Uptime
Kuma

+ + + + Wazuh +
+

Wazuh

+
@@ -443,6 +660,20 @@

What's
Up
Docker

+ + + + xTeVe +
+

xTeVe

+
+ + + + Your Spotify +
+

Your
Spotify

+
@@ -456,7 +687,7 @@ ![](img/headers/installation.png)

- + Please click here for official installation instructions

From 67d76f034d0d82dab8d132b5d125b70423aa10a1 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 3 Oct 2026 20:19:28 +0200 Subject: [PATCH 15/29] fix: use native DNS resolution by default for Docker services (#6997) * fix: use native DNS resolution by default for Docker services * test: verify Docker service DNS through production Compose * test: isolate Docker DNS smoke resources per invocation [skip ci] --- .../advanced/environment-variables/index.mdx | 2 +- .../docs/management/integrations/index.mdx | 23 ++ docs/research/docker-network-hostnames.md | 86 +++++++ packages/core/src/infrastructure/dns/env.ts | 2 +- packages/core/src/infrastructure/dns/init.ts | 18 +- .../src/test/infrastructure/dns/init.spec.ts | 56 +++++ scripts/smoke/docker-network.compose.yml | 75 ++++++ scripts/smoke/docker-network.mts | 232 ++++++++++++++++++ 8 files changed, 483 insertions(+), 11 deletions(-) create mode 100644 docs/research/docker-network-hostnames.md create mode 100644 packages/core/src/test/infrastructure/dns/init.spec.ts create mode 100644 scripts/smoke/docker-network.compose.yml create mode 100644 scripts/smoke/docker-network.mts diff --git a/apps/docs/docs/advanced/environment-variables/index.mdx b/apps/docs/docs/advanced/environment-variables/index.mdx index 57074b5db..0474ef6c1 100644 --- a/apps/docs/docs/advanced/environment-variables/index.mdx +++ b/apps/docs/docs/advanced/environment-variables/index.mdx @@ -36,7 +36,7 @@ Using the `PUID` and `PGID` will require you to set the correct permissions on t | `LOG_LEVEL` | Log level to use | `debug` / `info` / `warn` / `error` | `info` | | `BASE_URL` | Public Homarr origin used by MCP OAuth metadata and redirects. Set this when a reverse proxy does not preserve the public host and protocol. | HTTP(S) origin without a path | - | | `NO_EXTERNAL_CONNECTION` | Disables Homarr's internet-backed requests, including update checks, weather, location search, and RSS feeds | `true` or `false` | `false` | -| `ENABLE_DNS_CACHING` | Enables dns caching. Enabled by default, but if you experience issues with IPv6 or static IPs, see [#4006](https://github.com/homarr-labs/homarr/issues/4006) or disable this flag | `true` or `false` | `true` | +| `ENABLE_DNS_CACHING` | Opt-in DNS cache. Keep disabled for Docker service names and `extra_hosts`; cached addresses can survive container recreation and DNS-first lookup can override hosts-file entries. | `true` or `false` | `false` | | `HOMARR_DISABLE_IPV6` | Disables the IPv6 listener of the internal nginx proxy, even when the host supports IPv6. IPv6 is normally disabled automatically when the host has no IPv6 configured. See [#4596](https://github.com/homarr-labs/homarr/issues/4596) | `true` or `false` | `false` | | `HOMARR_WEBSITE_URL` | Main Homarr documentation/site base used by runtime links. Must be an absolute HTTP(S) URL. | HTTP(S) URL | `https://homarr.dev` | | `WORKSHOP_API_URL` | PocketBase API used by Homarr and standalone documentation. | HTTP(S) URL | `HOMARR_WEBSITE_URL` | diff --git a/apps/docs/docs/management/integrations/index.mdx b/apps/docs/docs/management/integrations/index.mdx index f934ba268..1a2e2c06f 100644 --- a/apps/docs/docs/management/integrations/index.mdx +++ b/apps/docs/docs/management/integrations/index.mdx @@ -67,6 +67,29 @@ or groups. Lists are filtered on the server and only include connections the cur ## Connection problems +### Docker service URLs + +Use the service name and **container port** when Homarr and the service share a user-defined Docker network. +For example, Sonarr with `ports: ["6767:8989"]` is reachable from Homarr at `http://sonarr:8989`. +Use `http://sonarr:6767` only if Sonarr actually listens on `6767` inside its container. +The browser-facing app URL can use the published host port or reverse proxy. +See [Docker Compose networking](https://docs.docker.com/compose/how-tos/networking/). + +Homarr uses native name resolution by default. Leave `ENABLE_DNS_CACHING` unset or set it to `false`: +the optional cache can retain an old IP after a container is recreated and can override `extra_hosts` entries. +If an outbound proxy is configured, exclude local service names with `NO_PROXY`, for example `sonarr,radarr,jellyfin`. + +To check Docker DNS independently of Homarr, replace `homarr` with your container name: + +```sh +docker exec homarr node -e 'require("node:dns").lookup("sonarr", { all: true }, (error, addresses) => console.log(error?.code ?? addresses))' +``` + +`ENOTFOUND` here indicates a container resolver or network problem. Check both services' network membership and +Docker's embedded DNS; changing a Next.js setting cannot repair that resolver. + +### Other connection failures + If a connection test fails: 1. Check the integration page for the required base URL and credentials. diff --git a/docs/research/docker-network-hostnames.md b/docs/research/docker-network-hostnames.md new file mode 100644 index 000000000..96b83dfe7 --- /dev/null +++ b/docs/research/docker-network-hostnames.md @@ -0,0 +1,86 @@ +# Docker service hostname investigation + +Investigated against `dev` on 2026-10-03. The fix makes Homarr DNS caching opt-in and leaves native +name resolution active by default. + +## Confirmed application failure + +Homarr's HTTP agent imports the DNS initializer, which previously enabled `dns-caching@0.2.9` +globally with a minimum five-minute TTL. The dependency replaces callback and promise `dns.lookup` +with direct A/AAAA queries and caches the resulting address. It consults the OS resolver only after +DNS fails. This also changes `/etc/hosts` precedence and can bypass Compose `extra_hosts` entries. + +Sources: [published dependency source](https://registry.npmjs.org/dns-caching/-/dns-caching-0.2.9.tgz), +[Node DNS implementation considerations](https://nodejs.org/docs/latest-v24.x/api/dns.html#implementation-considerations), +[Compose extra_hosts](https://docs.docker.com/reference/compose-file/services/#extra_hosts). + +A temporary user-defined Docker network reproduced stale-address reuse using Node 24.18.0, +Undici 7.29.1, and the actual bundled Homarr `UndiciHttpAgent` and DNS initializer: + +1. Start an HTTP fixture listening on container port `6767` with network alias `sonarr`. +2. Request `http://sonarr:6767` through Homarr's agent; receive HTTP 200. +3. Recreate only the fixture at a different IP, retaining the `sonarr` alias. +4. An original, native `dns.lookup` reference resolves the new IP immediately. +5. A new Homarr HTTP agent still connects using the cached IP and times out. + +The request agents were recreated between requests, ruling out pooled-socket reuse. Environment +validation was enabled so the initializer applied its real default. Proxies were explicitly bypassed +to isolate DNS. This initial reproduction used a bundled agent rather than the production image. + +Docker explicitly requires clients to look up a recreated service's new IP: +[Compose networking: update containers on the network](https://docs.docker.com/compose/how-tos/networking/#update-containers-on-the-network). + +## Separate deployment causes + +- Same-network URLs use the container listening port. A published mapping `6767:8989` requires + `http://sonarr:8989` internally. [Docker Compose networking](https://docs.docker.com/compose/how-tos/networking/). +- Native lookup in the existing local Homarr container returned `ENOTFOUND` for its neighboring + services, although direct-IP HTTP reached Sonarr on `8989`. Fresh temporary containers resolved + the same alias successfully. That is a separate Docker resolver failure; this PR cannot repair + existing daemon/network registrations. No existing services or networks were modified. +- Homarr's `EnvHttpProxyAgent` respects outbound proxy variables. Docker aliases need `NO_PROXY` + entries when the proxy cannot reach them. Preserve the operator's explicit proxy configuration. + [Undici 7.29.1 EnvHttpProxyAgent](https://github.com/nodejs/undici/blob/v7.29.1/docs/docs/api/EnvHttpProxyAgent.md). +- Next.js `images.dangerouslyAllowLocalIP` applies to image optimization. Integration requests use + Undici directly, so that setting does not solve integration connectivity. + [Next.js Image configuration](https://nextjs.org/docs/app/api-reference/components/image#dangerouslyallowlocalip). + +## Scope and tradeoff + +Default requests now use the OS resolver and see Docker address changes on new connections. +`ENABLE_DNS_CACHING=true` remains available for operators who deliberately accept cached addresses +and DNS-first resolution. This may increase DNS queries compared with the old default; HTTP connection +reuse remains unchanged. Existing explicit opt-ins must be removed or set to `false` to get native lookup. + +The same manual recreation probe passed after the default changed: both native lookup and Homarr's +HTTP agent followed the new service IP and received HTTP 200 without restarting the client process. + +## Production Compose acceptance + +The production Dockerfile was built locally and run with real Sonarr 4.0.19.2979 and Radarr 6.3.0.10514 +on a fresh user-defined Compose network. The smoke runner creates a disposable administrator/API key, +calls the real Next.js integration connection tests, and requests each service's `/ping` and authenticated +`/api/v3/system/status` through `/api/integrations/request`. All returned HTTP 200. + +The same Next.js process reached a health fixture on port `6767` after its IP changed. A conflicting +Docker DNS alias and `extra_hosts` entry confirmed native hosts-file precedence. Re-enabling +`ENABLE_DNS_CACHING=true` in the same production image reproduced both DNS-first hosts-file override +and a failed request after another fixture IP change, while a fresh native request still reached it. + +Run from the checkout: + +```sh +docker build -t homarr:dns-network-6997 . +node scripts/smoke/docker-network.mts +CI=true pnpm exec vitest run packages/core/src/test/infrastructure/dns/init.spec.ts packages/core/src/test/infrastructure/http/http-agent.spec.ts --project dom +``` + +The smoke runner uses a fresh `homarr-dns-` project, verifies its namespace is empty before +startup, and removes only that invocation's containers, network and disposable volumes afterward. +Homarr binds only to loopback; Sonarr/Radarr images are pinned. The fixed `10.203.199.0/24` subnet +prevents concurrent runs; a collision fails without cleaning another project. + +Focused DNS/HTTP tests passed (20 tests). Temporarily restoring the old default made the new +address-family contract fail, confirming that it detects a regression. The read-only review found no +actionable defects; explicit cache opt-ins and pre-existing Docker resolver failures remain outside +the corrected default behavior. diff --git a/packages/core/src/infrastructure/dns/env.ts b/packages/core/src/infrastructure/dns/env.ts index 2bc3abf38..96dd526b8 100644 --- a/packages/core/src/infrastructure/dns/env.ts +++ b/packages/core/src/infrastructure/dns/env.ts @@ -2,7 +2,7 @@ import { createBooleanSchema, createEnv } from "../env"; export const dnsEnv = createEnv({ server: { - ENABLE_DNS_CACHING: createBooleanSchema(true), + ENABLE_DNS_CACHING: createBooleanSchema(false), }, experimental__runtimeEnv: process.env, }); diff --git a/packages/core/src/infrastructure/dns/init.ts b/packages/core/src/infrastructure/dns/init.ts index fa9301576..20055fd00 100644 --- a/packages/core/src/infrastructure/dns/init.ts +++ b/packages/core/src/infrastructure/dns/init.ts @@ -14,15 +14,15 @@ declare global { const logger = createLogger({ module: "dns" }); -// Initialize global.homarr if not present -// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -global.homarr ??= {}; -global.homarr.dnsCacheManager ??= new DnsCacheManager({ - cacheMaxEntries: 1000, - forceMinTtl: 5 * 60 * 1000, // 5 minutes - logger, -}); - if (dnsEnv.ENABLE_DNS_CACHING) { + // Opt-in only: cached addresses can outlive Docker service endpoints, and + // DNS-first resolution does not preserve /etc/hosts precedence. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + global.homarr ??= {}; + global.homarr.dnsCacheManager ??= new DnsCacheManager({ + cacheMaxEntries: 1000, + forceMinTtl: 5 * 60 * 1000, // 5 minutes + logger, + }); global.homarr.dnsCacheManager.initialize(); } diff --git a/packages/core/src/test/infrastructure/dns/init.spec.ts b/packages/core/src/test/infrastructure/dns/init.spec.ts new file mode 100644 index 000000000..419639b28 --- /dev/null +++ b/packages/core/src/test/infrastructure/dns/init.spec.ts @@ -0,0 +1,56 @@ +// @vitest-environment node + +import dns from "node:dns"; +import { afterEach, beforeEach, expect, test, vi } from "vitest"; + +vi.mock("@homarr/core/infrastructure/logs", () => ({ + createLogger: () => ({ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }), +})); + +const nativeLookup = dns.lookup; +const nativePromiseLookup = dns.promises.lookup; + +beforeEach(() => { + vi.resetModules(); + // Exercise runtime validation, including the default, rather than CI's raw env values. + vi.stubEnv("CI", ""); + vi.stubEnv("SKIP_ENV_VALIDATION", ""); + vi.stubEnv("npm_lifecycle_event", "test"); + vi.stubEnv("ENABLE_DNS_CACHING", ""); +}); + +afterEach(() => { + dns.lookup = nativeLookup; + dns.promises.lookup = nativePromiseLookup; + delete global.homarr?.dnsCacheManager; + vi.unstubAllEnvs(); +}); + +test.each([undefined, "false"])( + "preserves native address-family filtering with DNS caching set to '%s'", + async (value) => { + vi.stubEnv("ENABLE_DNS_CACHING", value); + await import("@homarr/core/infrastructure/dns/init"); + const options = { family: 4, all: true } as const; + const expected = await nativePromiseLookup("localhost", options); + const addresses = await dns.promises.lookup("localhost", options); + expect(addresses).toEqual(expected); + expect(addresses.every(({ family }) => family === 4)).toBe(true); + + const callbackAddresses = await new Promise((resolve, reject) => { + dns.lookup("localhost", options, (error, result) => { + if (error) reject(error); + else resolve(result); + }); + }); + expect(callbackAddresses).toEqual(expected); + }, +); + +test("still enables DNS caching when explicitly requested", async () => { + vi.stubEnv("ENABLE_DNS_CACHING", "true"); + await import("@homarr/core/infrastructure/dns/init"); + expect(dns.lookup).not.toBe(nativeLookup); + expect(dns.promises.lookup).not.toBe(nativePromiseLookup); + expect(await dns.promises.lookup("localhost")).toEqual({ address: "127.0.0.1", family: 4 }); +}); diff --git a/scripts/smoke/docker-network.compose.yml b/scripts/smoke/docker-network.compose.yml new file mode 100644 index 000000000..e330f5a59 --- /dev/null +++ b/scripts/smoke/docker-network.compose.yml @@ -0,0 +1,75 @@ +services: + homarr: + image: ${HOMARR_DNS_IMAGE:-homarr:dns-network-6997} + environment: + SECRET_ENCRYPTION_KEY: "0000000000000000000000000000000000000000000000000000000000000000" + NO_EXTERNAL_CONNECTION: "true" + ports: + - "127.0.0.1::7575" + extra_hosts: + - "overridden-service:10.203.199.11" + volumes: + - homarr-data:/appdata + healthcheck: + test: + [ + "CMD", + "node", + "-e", + "fetch('http://127.0.0.1:7575/api/health/ready').then(r=>process.exit(r.status===200?0:1)).catch(()=>process.exit(1))", + ] + interval: 2s + timeout: 5s + retries: 90 + sonarr: + image: ghcr.io/hotio/sonarr@sha256:71d939c562a273767d97f51516f349e8459542cf868058e4b49891513210bdd5 + volumes: + - sonarr-data:/config + healthcheck: + test: ["CMD", "curl", "--fail", "--silent", "http://127.0.0.1:8989/ping"] + interval: 2s + timeout: 5s + retries: 90 + radarr: + image: ghcr.io/hotio/radarr@sha256:c054e15a3bb7a0fd9e450b12ceed1298a8230a0b44382234d0cec5e0767044a2 + volumes: + - radarr-data:/config + healthcheck: + test: ["CMD", "curl", "--fail", "--silent", "http://127.0.0.1:7878/ping"] + interval: 2s + timeout: 5s + retries: 90 + fixture: + image: node:24.18.0-alpine + environment: + PROOF_INSTANCE: ${PROOF_INSTANCE:-original} + command: + [ + "node", + "-e", + "require('node:http').createServer((req,res)=>{res.setHeader('content-type','application/json');res.end(JSON.stringify({healthy:true,instance:process.env.PROOF_INSTANCE}))}).listen(6767,'0.0.0.0')", + ] + networks: + default: + ipv4_address: ${PROOF_SERVICE_IP:-10.203.199.10} + aliases: ["overridden-service"] + hosts-override: + image: node:24.18.0-alpine + command: + [ + "node", + "-e", + "require('node:http').createServer((req,res)=>{res.setHeader('content-type','application/json');res.end(JSON.stringify({healthy:true,instance:'hosts-override'}))}).listen(6767,'0.0.0.0')", + ] + networks: + default: + ipv4_address: 10.203.199.11 +volumes: + homarr-data: + sonarr-data: + radarr-data: +networks: + default: + ipam: + config: + - subnet: 10.203.199.0/24 diff --git a/scripts/smoke/docker-network.mts b/scripts/smoke/docker-network.mts new file mode 100644 index 000000000..04727cc57 --- /dev/null +++ b/scripts/smoke/docker-network.mts @@ -0,0 +1,232 @@ +import assert from "node:assert/strict"; +import { execFile } from "node:child_process"; +import { randomUUID } from "node:crypto"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { resolve } from "node:path"; +import { promisify } from "node:util"; + +const execAsync = promisify(execFile); +const composeFile = resolve(import.meta.dirname, "docker-network.compose.yml"); +const project = `homarr-dns-${randomUUID()}`; +const temporaryDirectory = await mkdtemp(`${tmpdir()}/homarr-dns-proof-`); +const overrideFile = `${temporaryDirectory}/cache.json`; +let override = false; +let baseUrl = ""; +let apiKey = ""; +let ownsProject = false; + +async function assertUnusedProject() { + const label = `label=com.docker.compose.project=${project}`; + for (const resource of ["container", "network", "volume"]) { + const args = [resource, "ls", "--quiet", "--filter", label]; + if (resource === "container") args.push("--all"); + const result = await execAsync("docker", args); + assert.equal(result.stdout.trim(), "", "Disposable proof project already has resources; no mutation allowed"); + } +} + +async function compose(args: string[], environment: Record = {}) { + const files = ["-f", composeFile]; + if (override) files.push("-f", overrideFile); + const result = await execAsync("docker", ["compose", "-p", project, ...files, ...args], { + env: { ...process.env, ...environment }, + maxBuffer: 4 * 1024 * 1024, + }); + return result.stdout.trim(); +} + +async function refreshUrl() { + const address = await compose(["port", "homarr", "7575"]); + baseUrl = `http://${address}`; +} + +async function nextProcessIdentity() { + const identity = await compose([ + "exec", + "-T", + "homarr", + "node", + "-e", + ` + const fs = require('node:fs'); + for (const pid of fs.readdirSync('/proc').filter(value => /^\\d+$/.test(value))) { + try { + if (fs.readFileSync('/proc/'+pid+'/cmdline','utf8').startsWith('next-server')) { + const stat = fs.readFileSync('/proc/'+pid+'/stat','utf8'); + console.log(pid, stat.slice(stat.lastIndexOf(')')+2).split(' ')[19]); + } + } catch {} + } + `, + ]); + assert.ok(identity, "Next.js process was not found"); + return identity; +} + +async function api(path: string, input: unknown) { + const response = await fetch(`${baseUrl}${path}`, { + method: "POST", + headers: { "Content-Type": "application/json", ApiKey: apiKey }, + body: JSON.stringify(input), + signal: AbortSignal.timeout(25000), + }); + const result = await response.json(); + assert.equal(response.status, 200, `Homarr ${path} returned HTTP ${response.status}`); + return result; +} + +async function createIntegration(name: string, url: string, kind: string, token: string) { + const result = await api("/api/trpc/integration.create", { + json: { name, url, kind, secrets: [{ kind: "apiKey", value: token }], attemptSearchEngineCreation: false }, + }); + const data = result.result?.data?.json ?? result.result?.data; + const integration = data?.integration; + assert.ok( + integration?.id, + `Connection test/create failed for ${name}: ${JSON.stringify(data?.error?.type ?? result.error?.message ?? Object.keys(result))}`, + ); + console.log(`PASS Next.js connection test: ${name} at ${url}`); + return integration.id as string; +} + +async function request(integrationId: string, path: string) { + const result = await api("/api/integrations/request", { integrationId, path, method: "GET" }); + assert.equal(result.ok, true); + assert.equal(result.status, 200); + return result.data; +} + +async function upstreamKey(service: string) { + const config = await compose(["exec", "-T", service, "cat", "/config/config.xml"]); + const token = config.match(/([^<]+)<\/ApiKey>/)?.[1]; + assert.ok(token, `Missing API key for disposable ${service}`); + return token; +} + +async function recreateFixture(address: string, instance: string) { + await compose(["up", "-d", "--no-deps", "--force-recreate", "fixture"], { + PROOF_SERVICE_IP: address, + PROOF_INSTANCE: instance, + }); + const deadline = Date.now() + 10000; + while (true) { + try { + await compose([ + "exec", + "-T", + "fixture", + "node", + "-e", + "fetch('http://127.0.0.1:6767/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))", + ]); + return; + } catch { + assert.ok(Date.now() < deadline, "Recreated fixture never became ready"); + await new Promise((done) => setTimeout(done, 100)); + } + } +} + +try { + await assertUnusedProject(); + // The unpredictable, empty namespace belongs only to this invocation, including partial startup. + ownsProject = true; + await compose(["up", "-d", "--wait", "--wait-timeout", "180"]); + await refreshUrl(); + for (const path of ["/api/health/ready", "/api/health/live"]) { + const response = await fetch(`${baseUrl}${path}`); + assert.equal(response.status, 200); + console.log(`PASS production Homarr ${path}: HTTP 200`); + } + + // Provision only the disposable database. Keep the generated API key out of logs. + apiKey = await compose([ + "exec", + "-T", + "homarr", + "node", + "-e", + ` + const Database = require('better-sqlite3'); + const db = new Database(process.env.DB_URL, {nativeBinding:'/app/build/better_sqlite3.node'}); + const token = require('node:crypto').randomBytes(24).toString('hex'); + const hash = require('bcrypt').hashSync(token, 10); + db.transaction(() => { + db.prepare('INSERT OR IGNORE INTO user(id,name) VALUES (?,?)').run('dns-proof-user','DNS proof'); + db.prepare('INSERT OR IGNORE INTO "group"(id,name,position) VALUES (?,?,?)').run('dns-proof-group','DNS proof',999); + db.prepare('INSERT OR IGNORE INTO groupMember(group_id,user_id) VALUES (?,?)').run('dns-proof-group','dns-proof-user'); + db.prepare('DELETE FROM groupPermission WHERE group_id=?').run('dns-proof-group'); + db.prepare('INSERT INTO groupPermission(group_id,permission) VALUES (?,?)').run('dns-proof-group','admin'); + db.prepare('INSERT OR REPLACE INTO apiKey(id,api_key,user_id) VALUES (?,?,?)').run('dns-proof-key',hash,'dns-proof-user'); + })(); + db.close(); + process.stdout.write('dns-proof-key.'+token); + `, + ]); + + for (const [service, port] of [ + ["sonarr", 8989], + ["radarr", 7878], + ] as const) { + const id = await createIntegration(service, `http://${service}:${port}`, service, await upstreamKey(service)); + const health = await request(id, "/ping"); + assert.equal(health.status, "OK"); + const status = await request(id, "/api/v3/system/status"); + assert.ok(status.version); + console.log( + `PASS Next.js -> ${service}:${port}/ping and authenticated system/status: HTTP 200, version ${status.version}`, + ); + } + + const fixture = await createIntegration("DNS fixture", "http://fixture:6767", "sonarr", "fixture-token"); + const hosts = await createIntegration("Hosts override", "http://overridden-service:6767", "sonarr", "fixture-token"); + assert.equal((await request(hosts, "/health")).instance, "hosts-override"); + console.log("PASS extra_hosts takes precedence over the conflicting Docker DNS alias"); + assert.equal((await request(fixture, "/health")).instance, "original"); + const originalContainer = await compose(["ps", "-q", "homarr"]); + const originalNextProcess = await nextProcessIdentity(); + await recreateFixture("10.203.199.12", "replacement"); + assert.equal((await request(fixture, "/health")).instance, "replacement"); + assert.equal(await compose(["ps", "-q", "homarr"]), originalContainer); + assert.equal(await nextProcessIdentity(), originalNextProcess); + console.log("PASS fixture IP 10.203.199.10 -> 10.203.199.12: HTTP 200 without restarting Homarr"); + + // Re-enable the old default as a control, using the same production image and API routes. + await writeFile( + overrideFile, + JSON.stringify({ services: { homarr: { environment: { ENABLE_DNS_CACHING: "true" } } } }), + ); + override = true; + await compose(["up", "-d", "--no-deps", "--force-recreate", "--wait", "--wait-timeout", "90", "homarr"]); + await refreshUrl(); + assert.equal((await request(fixture, "/health")).instance, "replacement"); + assert.equal((await request(hosts, "/health")).instance, "replacement"); + console.log("PASS control ENABLE_DNS_CACHING=true reproduces DNS overriding extra_hosts"); + await recreateFixture("10.203.199.13", "control-replacement"); + const native = JSON.parse( + await compose([ + "exec", + "-T", + "homarr", + "node", + "-e", + "fetch('http://fixture:6767/health').then(async r=>console.log(JSON.stringify(await r.json()))).catch(()=>process.exit(1))", + ]), + ); + assert.equal(native.instance, "control-replacement"); + await assert.rejects( + request(fixture, "/health"), + (error: unknown) => error instanceof assert.AssertionError && [502, 504].includes(error.actual as number), + ); + console.log("PASS control ENABLE_DNS_CACHING=true reproduces failure after fixture IP change"); +} finally { + try { + if (ownsProject) { + await compose(["down", "--volumes"]); + console.log("Removed this invocation's disposable Compose stack and volumes"); + } + } finally { + await rm(temporaryDirectory, { recursive: true, force: true }); + } +} From a9820e99a4c33f49a6737216e88a2e7184043c14 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 3 Oct 2026 20:32:45 +0200 Subject: [PATCH 16/29] chore: trim supporting files from Docker DNS fix (#7011) [skip ci] --- .../docs/management/integrations/index.mdx | 23 -- docs/research/docker-network-hostnames.md | 86 ------- scripts/smoke/docker-network.compose.yml | 75 ------ scripts/smoke/docker-network.mts | 232 ------------------ 4 files changed, 416 deletions(-) delete mode 100644 docs/research/docker-network-hostnames.md delete mode 100644 scripts/smoke/docker-network.compose.yml delete mode 100644 scripts/smoke/docker-network.mts diff --git a/apps/docs/docs/management/integrations/index.mdx b/apps/docs/docs/management/integrations/index.mdx index 1a2e2c06f..f934ba268 100644 --- a/apps/docs/docs/management/integrations/index.mdx +++ b/apps/docs/docs/management/integrations/index.mdx @@ -67,29 +67,6 @@ or groups. Lists are filtered on the server and only include connections the cur ## Connection problems -### Docker service URLs - -Use the service name and **container port** when Homarr and the service share a user-defined Docker network. -For example, Sonarr with `ports: ["6767:8989"]` is reachable from Homarr at `http://sonarr:8989`. -Use `http://sonarr:6767` only if Sonarr actually listens on `6767` inside its container. -The browser-facing app URL can use the published host port or reverse proxy. -See [Docker Compose networking](https://docs.docker.com/compose/how-tos/networking/). - -Homarr uses native name resolution by default. Leave `ENABLE_DNS_CACHING` unset or set it to `false`: -the optional cache can retain an old IP after a container is recreated and can override `extra_hosts` entries. -If an outbound proxy is configured, exclude local service names with `NO_PROXY`, for example `sonarr,radarr,jellyfin`. - -To check Docker DNS independently of Homarr, replace `homarr` with your container name: - -```sh -docker exec homarr node -e 'require("node:dns").lookup("sonarr", { all: true }, (error, addresses) => console.log(error?.code ?? addresses))' -``` - -`ENOTFOUND` here indicates a container resolver or network problem. Check both services' network membership and -Docker's embedded DNS; changing a Next.js setting cannot repair that resolver. - -### Other connection failures - If a connection test fails: 1. Check the integration page for the required base URL and credentials. diff --git a/docs/research/docker-network-hostnames.md b/docs/research/docker-network-hostnames.md deleted file mode 100644 index 96b83dfe7..000000000 --- a/docs/research/docker-network-hostnames.md +++ /dev/null @@ -1,86 +0,0 @@ -# Docker service hostname investigation - -Investigated against `dev` on 2026-10-03. The fix makes Homarr DNS caching opt-in and leaves native -name resolution active by default. - -## Confirmed application failure - -Homarr's HTTP agent imports the DNS initializer, which previously enabled `dns-caching@0.2.9` -globally with a minimum five-minute TTL. The dependency replaces callback and promise `dns.lookup` -with direct A/AAAA queries and caches the resulting address. It consults the OS resolver only after -DNS fails. This also changes `/etc/hosts` precedence and can bypass Compose `extra_hosts` entries. - -Sources: [published dependency source](https://registry.npmjs.org/dns-caching/-/dns-caching-0.2.9.tgz), -[Node DNS implementation considerations](https://nodejs.org/docs/latest-v24.x/api/dns.html#implementation-considerations), -[Compose extra_hosts](https://docs.docker.com/reference/compose-file/services/#extra_hosts). - -A temporary user-defined Docker network reproduced stale-address reuse using Node 24.18.0, -Undici 7.29.1, and the actual bundled Homarr `UndiciHttpAgent` and DNS initializer: - -1. Start an HTTP fixture listening on container port `6767` with network alias `sonarr`. -2. Request `http://sonarr:6767` through Homarr's agent; receive HTTP 200. -3. Recreate only the fixture at a different IP, retaining the `sonarr` alias. -4. An original, native `dns.lookup` reference resolves the new IP immediately. -5. A new Homarr HTTP agent still connects using the cached IP and times out. - -The request agents were recreated between requests, ruling out pooled-socket reuse. Environment -validation was enabled so the initializer applied its real default. Proxies were explicitly bypassed -to isolate DNS. This initial reproduction used a bundled agent rather than the production image. - -Docker explicitly requires clients to look up a recreated service's new IP: -[Compose networking: update containers on the network](https://docs.docker.com/compose/how-tos/networking/#update-containers-on-the-network). - -## Separate deployment causes - -- Same-network URLs use the container listening port. A published mapping `6767:8989` requires - `http://sonarr:8989` internally. [Docker Compose networking](https://docs.docker.com/compose/how-tos/networking/). -- Native lookup in the existing local Homarr container returned `ENOTFOUND` for its neighboring - services, although direct-IP HTTP reached Sonarr on `8989`. Fresh temporary containers resolved - the same alias successfully. That is a separate Docker resolver failure; this PR cannot repair - existing daemon/network registrations. No existing services or networks were modified. -- Homarr's `EnvHttpProxyAgent` respects outbound proxy variables. Docker aliases need `NO_PROXY` - entries when the proxy cannot reach them. Preserve the operator's explicit proxy configuration. - [Undici 7.29.1 EnvHttpProxyAgent](https://github.com/nodejs/undici/blob/v7.29.1/docs/docs/api/EnvHttpProxyAgent.md). -- Next.js `images.dangerouslyAllowLocalIP` applies to image optimization. Integration requests use - Undici directly, so that setting does not solve integration connectivity. - [Next.js Image configuration](https://nextjs.org/docs/app/api-reference/components/image#dangerouslyallowlocalip). - -## Scope and tradeoff - -Default requests now use the OS resolver and see Docker address changes on new connections. -`ENABLE_DNS_CACHING=true` remains available for operators who deliberately accept cached addresses -and DNS-first resolution. This may increase DNS queries compared with the old default; HTTP connection -reuse remains unchanged. Existing explicit opt-ins must be removed or set to `false` to get native lookup. - -The same manual recreation probe passed after the default changed: both native lookup and Homarr's -HTTP agent followed the new service IP and received HTTP 200 without restarting the client process. - -## Production Compose acceptance - -The production Dockerfile was built locally and run with real Sonarr 4.0.19.2979 and Radarr 6.3.0.10514 -on a fresh user-defined Compose network. The smoke runner creates a disposable administrator/API key, -calls the real Next.js integration connection tests, and requests each service's `/ping` and authenticated -`/api/v3/system/status` through `/api/integrations/request`. All returned HTTP 200. - -The same Next.js process reached a health fixture on port `6767` after its IP changed. A conflicting -Docker DNS alias and `extra_hosts` entry confirmed native hosts-file precedence. Re-enabling -`ENABLE_DNS_CACHING=true` in the same production image reproduced both DNS-first hosts-file override -and a failed request after another fixture IP change, while a fresh native request still reached it. - -Run from the checkout: - -```sh -docker build -t homarr:dns-network-6997 . -node scripts/smoke/docker-network.mts -CI=true pnpm exec vitest run packages/core/src/test/infrastructure/dns/init.spec.ts packages/core/src/test/infrastructure/http/http-agent.spec.ts --project dom -``` - -The smoke runner uses a fresh `homarr-dns-` project, verifies its namespace is empty before -startup, and removes only that invocation's containers, network and disposable volumes afterward. -Homarr binds only to loopback; Sonarr/Radarr images are pinned. The fixed `10.203.199.0/24` subnet -prevents concurrent runs; a collision fails without cleaning another project. - -Focused DNS/HTTP tests passed (20 tests). Temporarily restoring the old default made the new -address-family contract fail, confirming that it detects a regression. The read-only review found no -actionable defects; explicit cache opt-ins and pre-existing Docker resolver failures remain outside -the corrected default behavior. diff --git a/scripts/smoke/docker-network.compose.yml b/scripts/smoke/docker-network.compose.yml deleted file mode 100644 index e330f5a59..000000000 --- a/scripts/smoke/docker-network.compose.yml +++ /dev/null @@ -1,75 +0,0 @@ -services: - homarr: - image: ${HOMARR_DNS_IMAGE:-homarr:dns-network-6997} - environment: - SECRET_ENCRYPTION_KEY: "0000000000000000000000000000000000000000000000000000000000000000" - NO_EXTERNAL_CONNECTION: "true" - ports: - - "127.0.0.1::7575" - extra_hosts: - - "overridden-service:10.203.199.11" - volumes: - - homarr-data:/appdata - healthcheck: - test: - [ - "CMD", - "node", - "-e", - "fetch('http://127.0.0.1:7575/api/health/ready').then(r=>process.exit(r.status===200?0:1)).catch(()=>process.exit(1))", - ] - interval: 2s - timeout: 5s - retries: 90 - sonarr: - image: ghcr.io/hotio/sonarr@sha256:71d939c562a273767d97f51516f349e8459542cf868058e4b49891513210bdd5 - volumes: - - sonarr-data:/config - healthcheck: - test: ["CMD", "curl", "--fail", "--silent", "http://127.0.0.1:8989/ping"] - interval: 2s - timeout: 5s - retries: 90 - radarr: - image: ghcr.io/hotio/radarr@sha256:c054e15a3bb7a0fd9e450b12ceed1298a8230a0b44382234d0cec5e0767044a2 - volumes: - - radarr-data:/config - healthcheck: - test: ["CMD", "curl", "--fail", "--silent", "http://127.0.0.1:7878/ping"] - interval: 2s - timeout: 5s - retries: 90 - fixture: - image: node:24.18.0-alpine - environment: - PROOF_INSTANCE: ${PROOF_INSTANCE:-original} - command: - [ - "node", - "-e", - "require('node:http').createServer((req,res)=>{res.setHeader('content-type','application/json');res.end(JSON.stringify({healthy:true,instance:process.env.PROOF_INSTANCE}))}).listen(6767,'0.0.0.0')", - ] - networks: - default: - ipv4_address: ${PROOF_SERVICE_IP:-10.203.199.10} - aliases: ["overridden-service"] - hosts-override: - image: node:24.18.0-alpine - command: - [ - "node", - "-e", - "require('node:http').createServer((req,res)=>{res.setHeader('content-type','application/json');res.end(JSON.stringify({healthy:true,instance:'hosts-override'}))}).listen(6767,'0.0.0.0')", - ] - networks: - default: - ipv4_address: 10.203.199.11 -volumes: - homarr-data: - sonarr-data: - radarr-data: -networks: - default: - ipam: - config: - - subnet: 10.203.199.0/24 diff --git a/scripts/smoke/docker-network.mts b/scripts/smoke/docker-network.mts deleted file mode 100644 index 04727cc57..000000000 --- a/scripts/smoke/docker-network.mts +++ /dev/null @@ -1,232 +0,0 @@ -import assert from "node:assert/strict"; -import { execFile } from "node:child_process"; -import { randomUUID } from "node:crypto"; -import { mkdtemp, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { resolve } from "node:path"; -import { promisify } from "node:util"; - -const execAsync = promisify(execFile); -const composeFile = resolve(import.meta.dirname, "docker-network.compose.yml"); -const project = `homarr-dns-${randomUUID()}`; -const temporaryDirectory = await mkdtemp(`${tmpdir()}/homarr-dns-proof-`); -const overrideFile = `${temporaryDirectory}/cache.json`; -let override = false; -let baseUrl = ""; -let apiKey = ""; -let ownsProject = false; - -async function assertUnusedProject() { - const label = `label=com.docker.compose.project=${project}`; - for (const resource of ["container", "network", "volume"]) { - const args = [resource, "ls", "--quiet", "--filter", label]; - if (resource === "container") args.push("--all"); - const result = await execAsync("docker", args); - assert.equal(result.stdout.trim(), "", "Disposable proof project already has resources; no mutation allowed"); - } -} - -async function compose(args: string[], environment: Record = {}) { - const files = ["-f", composeFile]; - if (override) files.push("-f", overrideFile); - const result = await execAsync("docker", ["compose", "-p", project, ...files, ...args], { - env: { ...process.env, ...environment }, - maxBuffer: 4 * 1024 * 1024, - }); - return result.stdout.trim(); -} - -async function refreshUrl() { - const address = await compose(["port", "homarr", "7575"]); - baseUrl = `http://${address}`; -} - -async function nextProcessIdentity() { - const identity = await compose([ - "exec", - "-T", - "homarr", - "node", - "-e", - ` - const fs = require('node:fs'); - for (const pid of fs.readdirSync('/proc').filter(value => /^\\d+$/.test(value))) { - try { - if (fs.readFileSync('/proc/'+pid+'/cmdline','utf8').startsWith('next-server')) { - const stat = fs.readFileSync('/proc/'+pid+'/stat','utf8'); - console.log(pid, stat.slice(stat.lastIndexOf(')')+2).split(' ')[19]); - } - } catch {} - } - `, - ]); - assert.ok(identity, "Next.js process was not found"); - return identity; -} - -async function api(path: string, input: unknown) { - const response = await fetch(`${baseUrl}${path}`, { - method: "POST", - headers: { "Content-Type": "application/json", ApiKey: apiKey }, - body: JSON.stringify(input), - signal: AbortSignal.timeout(25000), - }); - const result = await response.json(); - assert.equal(response.status, 200, `Homarr ${path} returned HTTP ${response.status}`); - return result; -} - -async function createIntegration(name: string, url: string, kind: string, token: string) { - const result = await api("/api/trpc/integration.create", { - json: { name, url, kind, secrets: [{ kind: "apiKey", value: token }], attemptSearchEngineCreation: false }, - }); - const data = result.result?.data?.json ?? result.result?.data; - const integration = data?.integration; - assert.ok( - integration?.id, - `Connection test/create failed for ${name}: ${JSON.stringify(data?.error?.type ?? result.error?.message ?? Object.keys(result))}`, - ); - console.log(`PASS Next.js connection test: ${name} at ${url}`); - return integration.id as string; -} - -async function request(integrationId: string, path: string) { - const result = await api("/api/integrations/request", { integrationId, path, method: "GET" }); - assert.equal(result.ok, true); - assert.equal(result.status, 200); - return result.data; -} - -async function upstreamKey(service: string) { - const config = await compose(["exec", "-T", service, "cat", "/config/config.xml"]); - const token = config.match(/([^<]+)<\/ApiKey>/)?.[1]; - assert.ok(token, `Missing API key for disposable ${service}`); - return token; -} - -async function recreateFixture(address: string, instance: string) { - await compose(["up", "-d", "--no-deps", "--force-recreate", "fixture"], { - PROOF_SERVICE_IP: address, - PROOF_INSTANCE: instance, - }); - const deadline = Date.now() + 10000; - while (true) { - try { - await compose([ - "exec", - "-T", - "fixture", - "node", - "-e", - "fetch('http://127.0.0.1:6767/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))", - ]); - return; - } catch { - assert.ok(Date.now() < deadline, "Recreated fixture never became ready"); - await new Promise((done) => setTimeout(done, 100)); - } - } -} - -try { - await assertUnusedProject(); - // The unpredictable, empty namespace belongs only to this invocation, including partial startup. - ownsProject = true; - await compose(["up", "-d", "--wait", "--wait-timeout", "180"]); - await refreshUrl(); - for (const path of ["/api/health/ready", "/api/health/live"]) { - const response = await fetch(`${baseUrl}${path}`); - assert.equal(response.status, 200); - console.log(`PASS production Homarr ${path}: HTTP 200`); - } - - // Provision only the disposable database. Keep the generated API key out of logs. - apiKey = await compose([ - "exec", - "-T", - "homarr", - "node", - "-e", - ` - const Database = require('better-sqlite3'); - const db = new Database(process.env.DB_URL, {nativeBinding:'/app/build/better_sqlite3.node'}); - const token = require('node:crypto').randomBytes(24).toString('hex'); - const hash = require('bcrypt').hashSync(token, 10); - db.transaction(() => { - db.prepare('INSERT OR IGNORE INTO user(id,name) VALUES (?,?)').run('dns-proof-user','DNS proof'); - db.prepare('INSERT OR IGNORE INTO "group"(id,name,position) VALUES (?,?,?)').run('dns-proof-group','DNS proof',999); - db.prepare('INSERT OR IGNORE INTO groupMember(group_id,user_id) VALUES (?,?)').run('dns-proof-group','dns-proof-user'); - db.prepare('DELETE FROM groupPermission WHERE group_id=?').run('dns-proof-group'); - db.prepare('INSERT INTO groupPermission(group_id,permission) VALUES (?,?)').run('dns-proof-group','admin'); - db.prepare('INSERT OR REPLACE INTO apiKey(id,api_key,user_id) VALUES (?,?,?)').run('dns-proof-key',hash,'dns-proof-user'); - })(); - db.close(); - process.stdout.write('dns-proof-key.'+token); - `, - ]); - - for (const [service, port] of [ - ["sonarr", 8989], - ["radarr", 7878], - ] as const) { - const id = await createIntegration(service, `http://${service}:${port}`, service, await upstreamKey(service)); - const health = await request(id, "/ping"); - assert.equal(health.status, "OK"); - const status = await request(id, "/api/v3/system/status"); - assert.ok(status.version); - console.log( - `PASS Next.js -> ${service}:${port}/ping and authenticated system/status: HTTP 200, version ${status.version}`, - ); - } - - const fixture = await createIntegration("DNS fixture", "http://fixture:6767", "sonarr", "fixture-token"); - const hosts = await createIntegration("Hosts override", "http://overridden-service:6767", "sonarr", "fixture-token"); - assert.equal((await request(hosts, "/health")).instance, "hosts-override"); - console.log("PASS extra_hosts takes precedence over the conflicting Docker DNS alias"); - assert.equal((await request(fixture, "/health")).instance, "original"); - const originalContainer = await compose(["ps", "-q", "homarr"]); - const originalNextProcess = await nextProcessIdentity(); - await recreateFixture("10.203.199.12", "replacement"); - assert.equal((await request(fixture, "/health")).instance, "replacement"); - assert.equal(await compose(["ps", "-q", "homarr"]), originalContainer); - assert.equal(await nextProcessIdentity(), originalNextProcess); - console.log("PASS fixture IP 10.203.199.10 -> 10.203.199.12: HTTP 200 without restarting Homarr"); - - // Re-enable the old default as a control, using the same production image and API routes. - await writeFile( - overrideFile, - JSON.stringify({ services: { homarr: { environment: { ENABLE_DNS_CACHING: "true" } } } }), - ); - override = true; - await compose(["up", "-d", "--no-deps", "--force-recreate", "--wait", "--wait-timeout", "90", "homarr"]); - await refreshUrl(); - assert.equal((await request(fixture, "/health")).instance, "replacement"); - assert.equal((await request(hosts, "/health")).instance, "replacement"); - console.log("PASS control ENABLE_DNS_CACHING=true reproduces DNS overriding extra_hosts"); - await recreateFixture("10.203.199.13", "control-replacement"); - const native = JSON.parse( - await compose([ - "exec", - "-T", - "homarr", - "node", - "-e", - "fetch('http://fixture:6767/health').then(async r=>console.log(JSON.stringify(await r.json()))).catch(()=>process.exit(1))", - ]), - ); - assert.equal(native.instance, "control-replacement"); - await assert.rejects( - request(fixture, "/health"), - (error: unknown) => error instanceof assert.AssertionError && [502, 504].includes(error.actual as number), - ); - console.log("PASS control ENABLE_DNS_CACHING=true reproduces failure after fixture IP change"); -} finally { - try { - if (ownsProject) { - await compose(["down", "--volumes"]); - console.log("Removed this invocation's disposable Compose stack and volumes"); - } - } finally { - await rm(temporaryDirectory, { recursive: true, force: true }); - } -} From 55b203ad9302b643f3b12b6bf7858f7d4f488be9 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 3 Oct 2026 21:51:34 +0200 Subject: [PATCH 17/29] feat(api): test connections using stored integration credentials (#7014) --- packages/api/src/open-api.ts | 6 +- .../router/integration/integration-router.ts | 2 + .../integration-test-stored-connection.ts | 57 ++++++++++++ ...integration-test-stored-connection.spec.ts | 86 +++++++++++++++++++ 4 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 packages/api/src/router/integration/integration-test-stored-connection.ts create mode 100644 packages/api/src/router/test/integration/integration-test-stored-connection.spec.ts diff --git a/packages/api/src/open-api.ts b/packages/api/src/open-api.ts index b3530aa19..f398036b1 100644 --- a/packages/api/src/open-api.ts +++ b/packages/api/src/open-api.ts @@ -5,6 +5,7 @@ import { API_KEY_HEADER_NAME } from "@homarr/auth/api-key"; import { appRouter } from "./router/app"; import { boardRouter } from "./router/board"; import { integrationRequestProcedure } from "./router/integration/integration-request"; +import { integrationTestStoredConnectionProcedure } from "./router/integration/integration-test-stored-connection"; import { infoRouter } from "./router/info"; import { inviteRouter } from "./router/invite"; import { serverSettingsRouter } from "./router/serverSettings"; @@ -20,7 +21,10 @@ export const openApiRouter = createTRPCRouter({ inviteRouter, serverSettingsRouter, userRouter, - integration: createTRPCRouter({ request: integrationRequestProcedure }), + integration: createTRPCRouter({ + request: integrationRequestProcedure, + testConnection: integrationTestStoredConnectionProcedure, + }), }); export const openApiDocument = (base: string) => { diff --git a/packages/api/src/router/integration/integration-router.ts b/packages/api/src/router/integration/integration-router.ts index 9f6439207..5b540a4ca 100644 --- a/packages/api/src/router/integration/integration-router.ts +++ b/packages/api/src/router/integration/integration-router.ts @@ -44,6 +44,7 @@ import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publ import { throwIfActionForbiddenAsync } from "./integration-access"; import { integrationRequestProcedure } from "./integration-request"; import { MissingSecretError, testConnectionAsync } from "./integration-test-connection"; +import { integrationTestStoredConnectionProcedure } from "./integration-test-stored-connection"; import { mapTestConnectionError } from "./map-test-connection-error"; const logger = createLogger({ module: "integrationRouter" }); @@ -51,6 +52,7 @@ const mediaRequestSearchKinds = getIntegrationKindsByCategory("mediaSearch"); export const integrationRouter = createTRPCRouter({ request: integrationRequestProcedure, + testConnection: integrationTestStoredConnectionProcedure, getKinds: publicProcedure .meta({ mcp: { diff --git a/packages/api/src/router/integration/integration-test-stored-connection.ts b/packages/api/src/router/integration/integration-test-stored-connection.ts new file mode 100644 index 000000000..0395c0350 --- /dev/null +++ b/packages/api/src/router/integration/integration-test-stored-connection.ts @@ -0,0 +1,57 @@ +import { TRPCError } from "@trpc/server"; +import { z } from "zod/v4"; + +import { eq } from "@homarr/db"; +import { integrations } from "@homarr/db/schema"; +import { byIdSchema } from "@homarr/validation/common"; + +import { protectedProcedure } from "../../trpc"; +import { throwIfActionForbiddenAsync } from "./integration-access"; +import { MissingSecretError, testConnectionAsync } from "./integration-test-connection"; + +export const integrationTestStoredConnectionProcedure = protectedProcedure + .meta({ + openapi: { + method: "GET", + path: "/api/integrations/{id}/test-connection", + tags: ["integrations"], + protect: true, + summary: "Test a saved integration using stored credentials", + description: + "Requires full integration access. Runs the native connection check without changing saved configuration. Upstream authentication may establish a session. Success is a connection check, not widget or service acceptance. Failures expose only a safe category, not upstream errors or credentials.", + }, + }) + .input(byIdSchema) + .output( + z.discriminatedUnion("success", [ + z.object({ success: z.literal(true) }), + z.object({ + success: z.literal(false), + error: z.enum(["missingSecret", "authorization", "certificate", "request", "statusCode", "parse", "unknown"]), + }), + ]), + ) + .query(async ({ ctx, input }) => { + const where = eq(integrations.id, input.id); + await throwIfActionForbiddenAsync(ctx, where, "full"); + + const integration = await ctx.db.query.integrations.findFirst({ where, with: { secrets: true } }); + if (!integration) { + throw new TRPCError({ code: "NOT_FOUND", message: "Integration not found" }); + } + + try { + const result = await testConnectionAsync( + { + ...integration, + secrets: integration.secrets.map(({ kind }) => ({ kind, value: null })), + }, + integration.secrets, + ); + if (result.success) return { success: true as const }; + return { success: false as const, error: result.error.type }; + } catch (error) { + if (error instanceof MissingSecretError) return { success: false as const, error: "missingSecret" as const }; + return { success: false as const, error: "unknown" as const }; + } + }); diff --git a/packages/api/src/router/test/integration/integration-test-stored-connection.spec.ts b/packages/api/src/router/test/integration/integration-test-stored-connection.spec.ts new file mode 100644 index 000000000..22c1faded --- /dev/null +++ b/packages/api/src/router/test/integration/integration-test-stored-connection.spec.ts @@ -0,0 +1,86 @@ +import { beforeEach, expect, test, vi } from "vitest"; + +import type { Session } from "@homarr/auth"; +import { encryptSecret } from "@homarr/common/server"; +import { integrations, integrationSecrets } from "@homarr/db/schema"; +import { createDb } from "@homarr/db/test"; + +import { integrationTestStoredConnectionProcedure } from "../../integration/integration-test-stored-connection"; +import { createTRPCRouter } from "../../../trpc"; + +const { factory, upstreamTest } = vi.hoisted(() => ({ factory: vi.fn(), upstreamTest: vi.fn() })); +vi.mock("@homarr/integrations/factory", () => ({ createIntegrationAsync: factory })); + +beforeEach(() => { + factory.mockReset().mockResolvedValue({ testConnectionAsync: upstreamTest }); + upstreamTest.mockReset().mockResolvedValue({ success: true }); +}); + +const setup = async (permissions: Session["user"]["permissions"] = ["integration-full-all"]) => { + const db = createDb(); + await db.insert(integrations).values({ id: "saved", name: "Sonarr", kind: "sonarr", url: "http://sonarr:8989" }); + await db + .insert(integrationSecrets) + .values({ integrationId: "saved", kind: "apiKey", value: encryptSecret("private-key") }); + const session: Session = { + user: { id: "owner", permissions, colorScheme: "light" }, + expires: new Date().toISOString(), + }; + const router = createTRPCRouter({ testConnection: integrationTestStoredConnectionProcedure }); + return { db, router, caller: router.createCaller({ db, session, deviceType: undefined }) }; +}; + +test("tests stored credentials without modifying saved configuration", async () => { + const { db, caller } = await setup(); + const before = await db.query.integrations.findFirst({ with: { secrets: true } }); + expect(await caller.testConnection({ id: "saved" })).toEqual({ success: true }); + expect(factory).toHaveBeenCalledWith( + expect.objectContaining({ + id: "saved", + decryptedSecrets: [expect.objectContaining({ kind: "apiKey", value: "private-key" })], + }), + ); + expect(await db.query.integrations.findFirst({ with: { secrets: true } })).toEqual(before); +}); + +test("denies use-only access and hides missing integrations before loading secrets", async () => { + const { caller } = await setup(["integration-use-all"]); + await expect(caller.testConnection({ id: "saved" })).rejects.toMatchObject({ code: "NOT_FOUND" }); + await expect(caller.testConnection({ id: "missing" })).rejects.toMatchObject({ code: "NOT_FOUND" }); + expect(factory).not.toHaveBeenCalled(); +}); + +test("requires authentication", async () => { + const { db, router } = await setup(); + const caller = router.createCaller({ db, session: null, deviceType: undefined }); + await expect(caller.testConnection({ id: "saved" })).rejects.toMatchObject({ code: "UNAUTHORIZED" }); + expect(factory).not.toHaveBeenCalled(); +}); + +test("returns only safe categories for upstream failures and thrown errors", async () => { + const { caller } = await setup(); + upstreamTest.mockResolvedValueOnce({ + success: false, + error: { + type: "authorization", + message: "private-key", + data: { token: "private-key" }, + cause: new Error("private-key"), + }, + }); + expect(await caller.testConnection({ id: "saved" })).toEqual({ success: false, error: "authorization" }); + factory.mockRejectedValueOnce(new Error("private-key")); + expect(await caller.testConnection({ id: "saved" })).toEqual({ success: false, error: "unknown" }); + const { db } = await setup(); + await db.delete(integrationSecrets); + const router = createTRPCRouter({ testConnection: integrationTestStoredConnectionProcedure }); + const noSecretsCaller = router.createCaller({ + db, + session: { + user: { id: "owner", permissions: ["integration-full-all"], colorScheme: "light" }, + expires: new Date().toISOString(), + }, + deviceType: undefined, + }); + expect(await noSecretsCaller.testConnection({ id: "saved" })).toEqual({ success: false, error: "missingSecret" }); +}); From eec910aabb29959b9138a636cab160db14342f18 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 3 Oct 2026 22:32:45 +0200 Subject: [PATCH 18/29] fix: classify Plex music releases correctly (#6977) * fix: classify Plex music releases correctly * test(plex): cover music release classification --- apps/docs/docs/integrations/plex/index.mdx | 2 ++ .../integrations/src/plex/plex-integration.ts | 6 +++- .../src/plex/test/plex-integration.spec.ts | 28 +++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/apps/docs/docs/integrations/plex/index.mdx b/apps/docs/docs/integrations/plex/index.mdx index b6e35a6e3..eaf9bf1d6 100644 --- a/apps/docs/docs/integrations/plex/index.mdx +++ b/apps/docs/docs/integrations/plex/index.mdx @@ -19,6 +19,8 @@ import { mediaReleasesWidget } from "@site/docs/widgets/media-releases"; +The Media releases widget classifies Plex albums, artists, and tracks as Music. + ### Adding the integration diff --git a/packages/integrations/src/plex/plex-integration.ts b/packages/integrations/src/plex/plex-integration.ts index 90f8e1357..deaf12bca 100644 --- a/packages/integrations/src/plex/plex-integration.ts +++ b/packages/integrations/src/plex/plex-integration.ts @@ -606,7 +606,7 @@ const identitySchema = z.object({ }), }); -const mapType = (type: string): "movie" | "tv" | "unknown" => { +const mapType = (type: string): "movie" | "tv" | "music" | "unknown" => { switch (type) { case "movie": return "movie"; @@ -614,6 +614,10 @@ const mapType = (type: string): "movie" | "tv" | "unknown" => { case "season": case "episode": return "tv"; + case "album": + case "artist": + case "track": + return "music"; default: return "unknown"; } diff --git a/packages/integrations/src/plex/test/plex-integration.spec.ts b/packages/integrations/src/plex/test/plex-integration.spec.ts index 9b66148e9..82f500095 100644 --- a/packages/integrations/src/plex/test/plex-integration.spec.ts +++ b/packages/integrations/src/plex/test/plex-integration.spec.ts @@ -71,6 +71,34 @@ beforeEach(() => { }); describe("PlexIntegration.getMediaReleasesAsync", () => { + test.each([ + ["album", "music"], + ["artist", "music"], + ["track", "music"], + ["movie", "movie"], + ["show", "tv"], + ["unsupported", "unknown"], + ])("classifies %s releases as %s", async (type, expectedType) => { + mockFetch.mockImplementation((url) => { + const urlString = String(url); + if (urlString.endsWith("/identity")) { + return respond({ MediaContainer: { machineIdentifier: "server-id" } }); + } + if (urlString.endsWith("/library/recentlyAdded")) { + return respond({ MediaContainer: { Metadata: [createMetadataItem(type, { type })] } }); + } + if (urlString.endsWith("/library/sections")) { + return respond({ MediaContainer: { Directory: [] } }); + } + throw new Error(`Unexpected Plex request: ${urlString}`); + }); + + const releases = await createIntegration().getMediaReleasesAsync(); + + expect(releases).toHaveLength(1); + expect(releases[0]).toMatchObject({ title: "Movie", type: expectedType }); + }); + test("uses the series title for TV seasons and episodes", async () => { mockFetch.mockImplementation((url) => { const urlString = String(url); From e048ae9cd78e4213220c170e6f510a42d98be509 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 3 Oct 2026 22:32:49 +0200 Subject: [PATCH 19/29] fix(board): restore layouts after column changes (#6979) * fix: preserve geometry across layout resizing * style: format layout migration snapshots --- apps/docs/docs/management/boards/index.mdx | 5 + packages/api/src/router/board.ts | 42 +- .../api/src/router/onboard/onboard-router.ts | 2 + packages/boards/src/layout-preview.ts | 17 +- packages/common/src/index.ts | 2 + packages/common/src/layout-resize-source.ts | 79 + .../0016_add_layout_resize_source.sql | 1 + .../postgresql/meta/0016_snapshot.json | 2690 ++++++++++++++++ .../migrations/postgresql/meta/_journal.json | 7 + .../sqlite/0048_add_layout_resize_source.sql | 1 + .../migrations/sqlite/meta/0048_snapshot.json | 2722 +++++++++++++++++ .../db/migrations/sqlite/meta/_journal.json | 7 + packages/db/schema/postgresql.ts | 1 + packages/db/schema/sqlite.ts | 1 + 14 files changed, 5567 insertions(+), 10 deletions(-) create mode 100644 packages/common/src/layout-resize-source.ts create mode 100644 packages/db/migrations/postgresql/0016_add_layout_resize_source.sql create mode 100644 packages/db/migrations/postgresql/meta/0016_snapshot.json create mode 100644 packages/db/migrations/sqlite/0048_add_layout_resize_source.sql create mode 100644 packages/db/migrations/sqlite/meta/0048_snapshot.json diff --git a/apps/docs/docs/management/boards/index.mdx b/apps/docs/docs/management/boards/index.mdx index a319476c0..10fd76c4a 100644 --- a/apps/docs/docs/management/boards/index.mdx +++ b/apps/docs/docs/management/boards/index.mdx @@ -42,6 +42,11 @@ Tiles use fixed `200 × 200` logical-pixel cells with a `24px` gap. Homarr scale width instead of changing each widget's proportions. **Reset from Base** regenerates a Mobile or custom layout from the current Base layout. +Changing column or sidebar counts keeps a saved source arrangement while its generated positions remain unedited. +Widening back restores that arrangement, including Container sizes. Moving, resizing, adding, or removing content +starts a new source arrangement on the next layout resize. This cannot restore positions lost before this behavior +was introduced; widen the affected Container before moving widgets into its right-hand columns. + ### Containers and rails A **Container** groups apps, widgets, and nested Containers. It can have a label, collapse control, app-opening action, diff --git a/packages/api/src/router/board.ts b/packages/api/src/router/board.ts index 316cade53..6249d4db4 100644 --- a/packages/api/src/router/board.ts +++ b/packages/api/src/router/board.ts @@ -4,7 +4,12 @@ import { z } from "zod/v4"; import type { Session } from "@homarr/auth"; import { constructBoardPermissions } from "@homarr/auth/shared"; -import { createId, generateResponsiveGridFor } from "@homarr/common"; +import { + createId, + generateResponsiveGridFor, + getLayoutResizeSource, + serializeLayoutResizeSource, +} from "@homarr/common"; import type { GridAlgorithmItem } from "@homarr/common"; import type { DeviceType } from "@homarr/common/server"; import { createLogger } from "@homarr/core/infrastructure/logs"; @@ -864,6 +869,7 @@ export const boardRouter = createTRPCRouter({ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion id: layoutsMap.get(layout.id)!, boardId: newBoardId, + resizeSource: null, })); const sectionMap = new Map(boardSections.map((section) => [section.id, createId()])); @@ -1358,6 +1364,7 @@ export const boardRouter = createTRPCRouter({ const itemSectionLayoutsToUpdate: InferInsertModel[] = []; const sectionLayoutsToUpdate: InferInsertModel[] = []; const layoutsToUpdate = filterUpdatedItems(input.layouts, board.layouts); + const resizeSourcesToUpdate = new Map(); for (const updatedLayout of layoutsToUpdate) { const dbLayout = existingLayoutsById.get(updatedLayout.id); @@ -1386,6 +1393,7 @@ export const boardRouter = createTRPCRouter({ rightGutterColumnCount: updatedLayout.rightGutterColumnCount, }, }); + resizeSourcesToUpdate.set(updatedLayout.id, projectedLayout.resizeSource); itemSectionLayoutsToUpdate.push(...projectedLayout.itemSectionLayouts); sectionLayoutsToUpdate.push(...projectedLayout.sectionLayouts); } @@ -1441,6 +1449,7 @@ export const boardRouter = createTRPCRouter({ await transaction .update(schema.layouts) .set({ + ...getResizeSourceUpdate(resizeSourcesToUpdate, layout.id), name: layout.name, columnCount: layout.columnCount, leftGutterColumnCount: layout.leftGutterColumnCount, @@ -1499,6 +1508,7 @@ export const boardRouter = createTRPCRouter({ transaction .update(layouts) .set({ + ...getResizeSourceUpdate(resizeSourcesToUpdate, layout.id), name: layout.name, columnCount: layout.columnCount, leftGutterColumnCount: layout.leftGutterColumnCount, @@ -1564,6 +1574,10 @@ export const boardRouter = createTRPCRouter({ await handleTransactionsAsync(ctx.db, { async handleAsync(db, schema) { await db.transaction(async (transaction) => { + await transaction + .update(schema.layouts) + .set({ resizeSource: null }) + .where(eq(schema.layouts.id, targetLayout.id)); await transaction.delete(schema.itemLayouts).where(eq(schema.itemLayouts.layoutId, targetLayout.id)); await transaction.delete(schema.sectionLayouts).where(eq(schema.sectionLayouts.layoutId, targetLayout.id)); if (projectedLayout.itemSectionLayouts.length > 0) { @@ -1576,6 +1590,7 @@ export const boardRouter = createTRPCRouter({ }, handleSync(db) { db.transaction((transaction) => { + transaction.update(layouts).set({ resizeSource: null }).where(eq(layouts.id, targetLayout.id)).run(); transaction.delete(itemLayouts).where(eq(itemLayouts.layoutId, targetLayout.id)).run(); transaction.delete(sectionLayouts).where(eq(sectionLayouts.layoutId, targetLayout.id)).run(); if (projectedLayout.itemSectionLayouts.length > 0) { @@ -2511,8 +2526,14 @@ const noBoardWithSimilarNameAsync = async (db: Database, name: string, ignoredId } }; +const getResizeSourceUpdate = (sources: ReadonlyMap, layoutId: string) => { + if (!sources.has(layoutId)) return {}; + return { resizeSource: sources.get(layoutId) ?? null }; +}; + interface BoardForLayoutProjection { id: string; + layouts: Array<{ id: string; resizeSource?: string | null }>; items: Array<{ id: string; layouts: Array<{ @@ -2547,7 +2568,13 @@ const getUpdatedBoardLayout = ( current: BoardLayoutGeometry; }, ) => { - const elements = options.previous.elements ?? getElementsForLayout(board, options.previous.layoutId); + const currentElements = options.previous.elements ?? getElementsForLayout(board, options.previous.layoutId); + const savedLayout = board.layouts.find((layout) => layout.id === options.previous.layoutId); + let serializedSource: string | null | undefined; + if (options.previous.layoutId === options.current.layoutId) serializedSource = savedLayout?.resizeSource; + const source = getLayoutResizeSource(options.previous, currentElements, serializedSource); + const elements = source.elements; + const previousGeometry = { ...source.geometry, layoutId: options.previous.layoutId }; const emptyRoots = board.sections .filter((section) => section.kind === "empty") .toSorted((first, second) => (first.yOffset ?? 0) - (second.yOffset ?? 0) || first.id.localeCompare(second.id)); @@ -2586,12 +2613,12 @@ const getUpdatedBoardLayout = ( const sourceLanes = boardLanes.filter( (sourceLane) => rootByLane.has(sourceLane) && - getBoardLaneColumnCount(options.previous, sourceLane) > 0 && + getBoardLaneColumnCount(previousGeometry, sourceLane) > 0 && targetLaneBySourceLane.get(sourceLane) === lane, ); const previousWidth = sourceLanes.length === 1 - ? getBoardLaneColumnCount(options.previous, sourceLanes[0] ?? "main") + ? getBoardLaneColumnCount(previousGeometry, sourceLanes[0] ?? "main") : Number.MAX_SAFE_INTEGER; return [ @@ -2644,6 +2671,7 @@ const getUpdatedBoardLayout = ( return { itemSectionLayouts: itemSectionLayoutsCollection, sectionLayouts: sectionLayoutsCollection, + resizeSource: serializeLayoutResizeSource(source, options.current, updatedElements), }; }; @@ -2948,7 +2976,11 @@ const getFullBoardWithWhereAsync = async ( return { ...otherBoardProperties, layouts: layouts - .map(({ boardId: _, ...layout }) => layout) + .map(({ boardId: _, resizeSource, ...layout }) => { + const result: typeof layout & { resizeSource?: string | null } = layout; + if (resizeSource) result.resizeSource = resizeSource; + return result; + }) .toSorted((layoutA, layoutB) => layoutA.breakpoint - layoutB.breakpoint), sections: sections.map(({ collapseStates, ...section }) => parseSection({ diff --git a/packages/api/src/router/onboard/onboard-router.ts b/packages/api/src/router/onboard/onboard-router.ts index 6ad7e40a1..2b6b0b73d 100644 --- a/packages/api/src/router/onboard/onboard-router.ts +++ b/packages/api/src/router/onboard/onboard-router.ts @@ -352,6 +352,7 @@ export const onboardRouter = createTRPCRouter({ leftGutterColumnCount: 0, rightGutterColumnCount: 0, breakpoint: 0, + resizeSource: null, role: "mobile" as const, }, { @@ -362,6 +363,7 @@ export const onboardRouter = createTRPCRouter({ leftGutterColumnCount: 0, rightGutterColumnCount: 0, breakpoint: 768, + resizeSource: null, role: "base" as const, }, ] diff --git a/packages/boards/src/layout-preview.ts b/packages/boards/src/layout-preview.ts index 278edb7c4..78b47ec7e 100644 --- a/packages/boards/src/layout-preview.ts +++ b/packages/boards/src/layout-preview.ts @@ -1,4 +1,4 @@ -import { generateResponsiveGridFor } from "@homarr/common"; +import { generateResponsiveGridFor, getLayoutResizeSource } from "@homarr/common"; import type { GridAlgorithmItem } from "@homarr/common"; import type { BoardLane, LayoutRole, WidgetKind } from "@homarr/definitions"; import { boardLanes, getBoardLaneColumnCount, getRootSectionLane } from "@homarr/definitions"; @@ -10,6 +10,7 @@ export interface BoardPreviewLayout { rightGutterColumnCount: number; breakpoint: number; role: LayoutRole; + resizeSource?: string | null; } interface BoardPreviewItemLayout { @@ -77,7 +78,13 @@ export const projectBoardLayout = ( sourceLayout: LayoutGeometry, targetLayout: LayoutGeometry, ) => { - const elements = getElementsForLayout(board, sourceLayout.id); + const currentElements = getElementsForLayout(board, sourceLayout.id); + let serializedSource: string | null | undefined; + if (sourceLayout.id === targetLayout.id) { + serializedSource = board.layouts.find((layout) => layout.id === sourceLayout.id)?.resizeSource; + } + const source = getLayoutResizeSource(sourceLayout, currentElements, serializedSource); + const elements = source.elements; if ( sourceLayout.id === targetLayout.id && @@ -85,7 +92,7 @@ export const projectBoardLayout = ( sourceLayout.leftGutterColumnCount === targetLayout.leftGutterColumnCount && sourceLayout.rightGutterColumnCount === targetLayout.rightGutterColumnCount ) { - return elements; + return currentElements; } const emptyRoots = board.sections @@ -124,12 +131,12 @@ export const projectBoardLayout = ( const sourceLanes = boardLanes.filter( (sourceLane) => rootByLane.has(sourceLane) && - getBoardLaneColumnCount(sourceLayout, sourceLane) > 0 && + getBoardLaneColumnCount(source.geometry, sourceLane) > 0 && targetLaneBySourceLane.get(sourceLane) === lane, ); const previousWidth = sourceLanes.length === 1 - ? getBoardLaneColumnCount(sourceLayout, sourceLanes[0] ?? "main") + ? getBoardLaneColumnCount(source.geometry, sourceLanes[0] ?? "main") : Number.MAX_SAFE_INTEGER; return generateResponsiveGridFor({ diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 19ed7ce44..8a7f039ac 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -12,3 +12,5 @@ export * from "./error"; export * from "./theme"; export * from "./function"; export * from "./grid-algorithm"; + +export * from "./layout-resize-source"; diff --git a/packages/common/src/layout-resize-source.ts b/packages/common/src/layout-resize-source.ts new file mode 100644 index 000000000..0458a22ec --- /dev/null +++ b/packages/common/src/layout-resize-source.ts @@ -0,0 +1,79 @@ +import { z } from "zod/v4"; + +import type { GridAlgorithmItem } from "./grid-algorithm"; + +const geometrySchema = z.object({ + columnCount: z.number().int().positive(), + leftGutterColumnCount: z.number().int().nonnegative(), + rightGutterColumnCount: z.number().int().nonnegative(), +}); +const elementSchema = z.object({ + id: z.string(), + type: z.enum(["item", "section"]), + sectionId: z.string(), + xOffset: z.number().int().nonnegative(), + yOffset: z.number().int().nonnegative(), + width: z.number().int().positive(), + height: z.number().int().positive(), +}); +const resizeSourceSchema = z.object({ + geometry: geometrySchema, + elements: z.array(elementSchema), + projectedGeometry: geometrySchema, + projectedElements: z.array(elementSchema), +}); + +type LayoutGeometry = z.infer; +export interface LayoutResizeSource { + geometry: LayoutGeometry; + elements: GridAlgorithmItem[]; +} + +const geometryKey = (geometry: LayoutGeometry) => + JSON.stringify([geometry.columnCount, geometry.leftGutterColumnCount, geometry.rightGutterColumnCount]); +const elementsKey = (elements: readonly GridAlgorithmItem[]) => + JSON.stringify( + elements + .map((element) => [ + element.id, + element.type, + element.sectionId, + element.xOffset, + element.yOffset, + element.width, + element.height, + ]) + .toSorted((first, second) => String(first[0]).localeCompare(String(second[0]))), + ); + +/** Keep the pre-resize arrangement only while the projected placements remain untouched. */ +export const getLayoutResizeSource = ( + geometry: LayoutGeometry, + elements: GridAlgorithmItem[], + serializedSource?: string | null, +): LayoutResizeSource => { + if (serializedSource) { + try { + const parsed = resizeSourceSchema.safeParse(JSON.parse(serializedSource)); + if ( + parsed.success && + geometryKey(parsed.data.projectedGeometry) === geometryKey(geometry) && + elementsKey(parsed.data.projectedElements) === elementsKey(elements) + ) { + return { geometry: parsed.data.geometry, elements: parsed.data.elements }; + } + } catch { + // Malformed or obsolete history must never prevent resizing the current layout. + } + } + return { geometry, elements }; +}; + +export const serializeLayoutResizeSource = ( + source: LayoutResizeSource, + projectedGeometry: LayoutGeometry, + projectedElements: GridAlgorithmItem[], +): string | null => { + if (geometryKey(source.geometry) === geometryKey(projectedGeometry)) return null; + return JSON.stringify({ ...source, projectedGeometry, projectedElements }); +}; diff --git a/packages/db/migrations/postgresql/0016_add_layout_resize_source.sql b/packages/db/migrations/postgresql/0016_add_layout_resize_source.sql new file mode 100644 index 000000000..0b7775fc5 --- /dev/null +++ b/packages/db/migrations/postgresql/0016_add_layout_resize_source.sql @@ -0,0 +1 @@ +ALTER TABLE "layout" ADD COLUMN "resize_source" text; diff --git a/packages/db/migrations/postgresql/meta/0016_snapshot.json b/packages/db/migrations/postgresql/meta/0016_snapshot.json new file mode 100644 index 000000000..0eb01557a --- /dev/null +++ b/packages/db/migrations/postgresql/meta/0016_snapshot.json @@ -0,0 +1,2690 @@ +{ + "id": "b61f0f83-6729-48c4-b44a-5379c3289ed0", + "prevId": "e2cbf7dc-3b51-4399-9c2f-10e128947d6f", + "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.assistant_configuration": { + "name": "assistant_configuration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true, + "default": "'default'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "web_search_enabled": { + "name": "web_search_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider": { + "name": "provider", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "default": "'openrouter'" + }, + "base_url": { + "name": "base_url", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": true, + "default": "'https://openrouter.ai/api/v1'" + }, + "model_discovery_path": { + "name": "model_discovery_path", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false, + "default": "'/models'" + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_headers": { + "name": "encrypted_headers", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_id": { + "name": "model_id", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assistant_message": { + "name": "assistant_message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": true, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "format": { + "name": "format", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "default": "'ai-sdk/v6'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "assistant_message__thread_id_created_at_idx": { + "name": "assistant_message__thread_id_created_at_idx", + "columns": [ + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assistant_message_thread_id_assistant_thread_id_fk": { + "name": "assistant_message_thread_id_assistant_thread_id_fk", + "tableFrom": "assistant_message", + "tableTo": "assistant_thread", + "columnsFrom": ["thread_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assistant_thread": { + "name": "assistant_thread", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "model_id": { + "name": "model_id", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "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()" + } + }, + "indexes": { + "assistant_thread__user_id_updated_at_idx": { + "name": "assistant_thread__user_id_updated_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assistant_thread_user_id_user_id_fk": { + "name": "assistant_thread_user_id_user_id_fk", + "tableFrom": "assistant_thread", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "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_v2_definition": { + "name": "custom_widget_v2_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 + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requests": { + "name": "requests", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template": { + "name": "template", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "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 + } + }, + "indexes": {}, + "foreignKeys": { + "custom_widget_v2_definition_creator_id_user_id_fk": { + "name": "custom_widget_v2_definition_creator_id_user_id_fk", + "tableFrom": "custom_widget_v2_definition", + "tableTo": "user", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_widget_v2_secret": { + "name": "custom_widget_v2_secret", + "schema": "", + "columns": { + "source_id": { + "name": "source_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "encrypted_value": { + "name": "encrypted_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_v2_secret_definition_id_fk": { + "name": "custom_widget_v2_secret_definition_id_fk", + "tableFrom": "custom_widget_v2_secret", + "tableTo": "custom_widget_v2_definition", + "columnsFrom": ["definition_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "custom_widget_v2_secret_definition_id_source_id_kind_pk": { + "name": "custom_widget_v2_secret_definition_id_source_id_kind_pk", + "columns": ["definition_id", "source_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 + }, + "left_gutter_column_count": { + "name": "left_gutter_column_count", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "right_gutter_column_count": { + "name": "right_gutter_column_count", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "breakpoint": { + "name": "breakpoint", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "role": { + "name": "role", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'custom'" + }, + "resize_source": { + "name": "resize_source", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "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.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 + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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\": {}}'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "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 + } + }, + "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.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'" + }, + "byte_unit_system": { + "name": "byte_unit_system", + "type": "varchar(7)", + "primaryKey": false, + "notNull": true, + "default": "'decimal'" + }, + "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 + }, + "enable_right_click_on_widgets": { + "name": "enable_right_click_on_widgets", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "header_preferences": { + "name": "header_preferences", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{\"version\":3,\"visible\":true,\"searchDisplay\":\"input\",\"logoDisplay\":\"logoAndText\",\"zones\":{\"left\":[{\"type\":\"builtin\",\"id\":\"logo\"}],\"center\":[{\"type\":\"builtin\",\"id\":\"search\"}],\"right\":[{\"type\":\"builtin\",\"id\":\"boardEdit\"},{\"type\":\"builtin\",\"id\":\"boardSettings\"},{\"type\":\"builtin\",\"id\":\"user\"}]}}'" + }, + "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.widget_secret": { + "name": "widget_secret", + "schema": "", + "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", + "columnsFrom": ["item_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "widget_secret_item_id_kind_pk": { + "name": "widget_secret_item_id_kind_pk", + "columns": ["item_id", "kind"] + } + }, + "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/_journal.json b/packages/db/migrations/postgresql/meta/_journal.json index c4b738d71..cf0263b27 100644 --- a/packages/db/migrations/postgresql/meta/_journal.json +++ b/packages/db/migrations/postgresql/meta/_journal.json @@ -113,6 +113,13 @@ "when": 1788763848932, "tag": "0015_add_byte_unit_system", "breakpoints": true + }, + { + "idx": 16, + "version": "7", + "when": 1790988597957, + "tag": "0016_add_layout_resize_source", + "breakpoints": true } ] } diff --git a/packages/db/migrations/sqlite/0048_add_layout_resize_source.sql b/packages/db/migrations/sqlite/0048_add_layout_resize_source.sql new file mode 100644 index 000000000..6f82337dd --- /dev/null +++ b/packages/db/migrations/sqlite/0048_add_layout_resize_source.sql @@ -0,0 +1 @@ +ALTER TABLE `layout` ADD `resize_source` text; diff --git a/packages/db/migrations/sqlite/meta/0048_snapshot.json b/packages/db/migrations/sqlite/meta/0048_snapshot.json new file mode 100644 index 000000000..8a8335c64 --- /dev/null +++ b/packages/db/migrations/sqlite/meta/0048_snapshot.json @@ -0,0 +1,2722 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "9afd1ddb-3537-4b7b-b3a7-957c2b94a437", + "prevId": "ce0d704e-f0a2-4098-b9b5-941051437bc1", + "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": {} + }, + "assistant_configuration": { + "name": "assistant_configuration", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": "'default'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "web_search_enabled": { + "name": "web_search_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'openrouter'" + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'https://openrouter.ai/api/v1'" + }, + "model_discovery_path": { + "name": "model_discovery_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'/models'" + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_headers": { + "name": "encrypted_headers", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "assistant_message": { + "name": "assistant_message", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ai-sdk/v6'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "assistant_message__thread_id_created_at_idx": { + "name": "assistant_message__thread_id_created_at_idx", + "columns": ["thread_id", "created_at"], + "isUnique": false + } + }, + "foreignKeys": { + "assistant_message_thread_id_assistant_thread_id_fk": { + "name": "assistant_message_thread_id_assistant_thread_id_fk", + "tableFrom": "assistant_message", + "tableTo": "assistant_thread", + "columnsFrom": ["thread_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "assistant_thread": { + "name": "assistant_thread", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "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())" + } + }, + "indexes": { + "assistant_thread__user_id_updated_at_idx": { + "name": "assistant_thread__user_id_updated_at_idx", + "columns": ["user_id", "updated_at"], + "isUnique": false + } + }, + "foreignKeys": { + "assistant_thread_user_id_user_id_fk": { + "name": "assistant_thread_user_id_user_id_fk", + "tableFrom": "assistant_thread", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "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_v2_definition": { + "name": "custom_widget_v2_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 + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "requests": { + "name": "requests", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "template": { + "name": "template", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "custom_widget_v2_definition_creator_id_user_id_fk": { + "name": "custom_widget_v2_definition_creator_id_user_id_fk", + "tableFrom": "custom_widget_v2_definition", + "tableTo": "user", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "custom_widget_v2_secret": { + "name": "custom_widget_v2_secret", + "columns": { + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_value": { + "name": "encrypted_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_v2_secret_definition_id_fk": { + "name": "custom_widget_v2_secret_definition_id_fk", + "tableFrom": "custom_widget_v2_secret", + "tableTo": "custom_widget_v2_definition", + "columnsFrom": ["definition_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "custom_widget_v2_secret_definition_id_source_id_kind_pk": { + "columns": ["definition_id", "source_id", "kind"], + "name": "custom_widget_v2_secret_definition_id_source_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 + }, + "left_gutter_column_count": { + "name": "left_gutter_column_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "right_gutter_column_count": { + "name": "right_gutter_column_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "breakpoint": { + "name": "breakpoint", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'custom'" + }, + "resize_source": { + "name": "resize_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "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": {} + }, + "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 + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "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\": {}}'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "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": {} + }, + "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'" + }, + "byte_unit_system": { + "name": "byte_unit_system", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'decimal'" + }, + "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 + }, + "enable_right_click_on_widgets": { + "name": "enable_right_click_on_widgets", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "header_preferences": { + "name": "header_preferences", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{\"version\":3,\"visible\":true,\"searchDisplay\":\"input\",\"logoDisplay\":\"logoAndText\",\"zones\":{\"left\":[{\"type\":\"builtin\",\"id\":\"logo\"}],\"center\":[{\"type\":\"builtin\",\"id\":\"search\"}],\"right\":[{\"type\":\"builtin\",\"id\":\"boardEdit\"},{\"type\":\"builtin\",\"id\":\"boardSettings\"},{\"type\":\"builtin\",\"id\":\"user\"}]}}'" + }, + "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/db/migrations/sqlite/meta/_journal.json b/packages/db/migrations/sqlite/meta/_journal.json index e13336310..72102f311 100644 --- a/packages/db/migrations/sqlite/meta/_journal.json +++ b/packages/db/migrations/sqlite/meta/_journal.json @@ -337,6 +337,13 @@ "when": 1788763848930, "tag": "0047_add_byte_unit_system", "breakpoints": true + }, + { + "idx": 48, + "version": "6", + "when": 1790988595595, + "tag": "0048_add_layout_resize_source", + "breakpoints": true } ] } diff --git a/packages/db/schema/postgresql.ts b/packages/db/schema/postgresql.ts index a515126ba..f37c6bca6 100644 --- a/packages/db/schema/postgresql.ts +++ b/packages/db/schema/postgresql.ts @@ -349,6 +349,7 @@ export const layouts = pgTable("layout", { rightGutterColumnCount: smallint().notNull().default(0), breakpoint: smallint().notNull().default(0), role: varchar({ length: 16 }).$type().notNull().default("custom"), + resizeSource: text(), }); export const itemLayouts = pgTable( diff --git a/packages/db/schema/sqlite.ts b/packages/db/schema/sqlite.ts index f2142d269..302299ac8 100644 --- a/packages/db/schema/sqlite.ts +++ b/packages/db/schema/sqlite.ts @@ -334,6 +334,7 @@ export const layouts = sqliteTable("layout", { rightGutterColumnCount: int().notNull().default(0), breakpoint: int().notNull().default(0), role: text().$type().notNull().default("custom"), + resizeSource: text(), }); export const itemLayouts = sqliteTable( From 121a50cb194eef49b104752f80f71d9b4efcb967 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 3 Oct 2026 22:33:30 +0200 Subject: [PATCH 20/29] fix(integrations): accept persisted IDs when updating (#6985) --- apps/docs/docs/management/integrations/index.mdx | 3 +++ packages/validation/src/integration.ts | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/docs/docs/management/integrations/index.mdx b/apps/docs/docs/management/integrations/index.mdx index f934ba268..a621bb855 100644 --- a/apps/docs/docs/management/integrations/index.mdx +++ b/apps/docs/docs/management/integrations/index.mdx @@ -52,6 +52,9 @@ used by widgets that open the service. Integrations can also be created while adding content to a board. Editing a connection from a widget updates that integration everywhere it is used. +Existing integration IDs are preserved when editing, including IDs containing hyphens from externally seeded data. +Changing settings requires full access to that integration; IDs do not grant access. + ## Permissions | Level | Allows | diff --git a/packages/validation/src/integration.ts b/packages/validation/src/integration.ts index c45b4ef73..8481a45ba 100644 --- a/packages/validation/src/integration.ts +++ b/packages/validation/src/integration.ts @@ -29,7 +29,8 @@ export const integrationCreateSchema = z.object({ }); export const integrationUpdateSchema = z.object({ - id: z.string().cuid2(), + // Updates address an existing row, including externally seeded IDs. + id: z.string().nonempty(), name: z.string().nonempty().max(127), url: z.string().url(), secrets: z.array( From 27a7f5f42e92b462851cfb51de689e6715d4dde3 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 3 Oct 2026 22:33:33 +0200 Subject: [PATCH 21/29] docs(migration): explain backups and board scaling (#6991) * docs(migration): explain backups and board scaling * docs: qualify migration layout restoration --- .../docs/blog/2026/09-03-homarr-2.0/index.mdx | 2 +- .../getting-started/installation/docker.mdx | 34 +++++++++++++++++-- apps/docs/docs/management/boards/index.mdx | 4 +-- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/apps/docs/blog/2026/09-03-homarr-2.0/index.mdx b/apps/docs/blog/2026/09-03-homarr-2.0/index.mdx index 6b376c7ee..ece68059e 100644 --- a/apps/docs/blog/2026/09-03-homarr-2.0/index.mdx +++ b/apps/docs/blog/2026/09-03-homarr-2.0/index.mdx @@ -43,7 +43,7 @@ Homarr 2.0 builds on the dashboard you already use. Here is what changed. {/* Historical excerpt boundary. */} -SQLite and PostgreSQL users can update their Homarr image as usual; database migrations run automatically at startup. **MySQL users must convert before upgrading**, as explained below. Workshop uses the production service by default, with no preview URL configuration required. +**Back up before upgrading:** database migrations run automatically at startup and change the existing database in place. Follow the [upgrade and rollback guidance](/docs/getting-started/installation/docker#update), including the note on board sizing. **MySQL users must convert before upgrading**, as explained below. Workshop uses the production service by default, with no preview URL configuration required. ## Database support in v2 diff --git a/apps/docs/docs/getting-started/installation/docker.mdx b/apps/docs/docs/getting-started/installation/docker.mdx index d9371261b..dd4898b4c 100644 --- a/apps/docs/docs/getting-started/installation/docker.mdx +++ b/apps/docs/docs/getting-started/installation/docker.mdx @@ -24,15 +24,44 @@ see [Managing integrations](/docs/management/integrations#configuration). ### Update + +**Back up before upgrading from v1 to v2.** v2 migrates the existing database in place. For SQLite, this is +`/appdata/db/db.sqlite` by default; v1 cannot use the upgraded database. Switching back to the v1 image alone is not a +rollback. Image-rollback tools that revert only the container image do not restore the database; you still need +the pre-upgrade backup. + +Stop Homarr before copying the complete `/appdata` directory or volume. Keep the pre-upgrade copy, your container +configuration, and `SECRET_ENCRYPTION_KEY` securely. If you use an external PostgreSQL database, back it up with its +native tools as well. See [Backup and restore](/docs/management/backup). + + + Update the image and recreate the container with the same configuration. Homarr runs database migrations automatically at startup, including when moving from v1 to v2; no separate upgrade procedure is needed. Keep the `/appdata` mount and `SECRET_ENCRYPTION_KEY` unchanged. Workshop connects to the production service by default. +Existing MySQL installations must [convert to SQLite before upgrading to v2](/docs/advanced/mysql-to-sqlite). + ```sh docker compose pull docker compose up -d ``` +To roll back, stop v2, restore the **pre-upgrade** database and `/appdata` backup with the matching configuration and +encryption key, then start the previously used v1 image. A backup taken after v2 has migrated the database cannot +restore v1 compatibility. + +#### Board sizing after upgrading to v2 + +v2 uses fixed `200 × 200` logical-pixel cells and scales the whole board to fit the available width. A migrated +12-column layout can therefore show smaller widgets and text than in v1. Reducing the column count in +[board layout settings](/docs/management/boards#responsive-layouts) makes cells appear larger at the same viewport +width; choose a count that fits your content. + +Back up before changing columns: reducing the count can resize Containers and displace widgets. Check the layout in +edit mode before saving. In v2.0.0, increasing the count again may not restore the previous arrangement after widgets +or Containers have been displaced or resized; keep a backup rather than relying on reversing the column change. + ### Uninstall `docker compose down` removes the containers and Compose network. Delete the configured `/appdata` host directory or @@ -53,5 +82,6 @@ docker run \ -d ghcr.io/homarr-labs/homarr:latest ``` -Remove the Docker socket mount when it is not required. To update, pull the current image, recreate the container with -the same arguments, and keep the `/appdata` mount and `SECRET_ENCRYPTION_KEY` unchanged. +Remove the Docker socket mount when it is not required. To update, follow the [backup guidance above](#update), pull +the current image, recreate the container with the same arguments, and keep the `/appdata` mount and +`SECRET_ENCRYPTION_KEY` unchanged. diff --git a/apps/docs/docs/management/boards/index.mdx b/apps/docs/docs/management/boards/index.mdx index 10fd76c4a..effa13748 100644 --- a/apps/docs/docs/management/boards/index.mdx +++ b/apps/docs/docs/management/boards/index.mdx @@ -38,8 +38,8 @@ Mobile and Base are protected layouts. New boards use three Mobile columns; Base creation. Additional layouts use unique breakpoints. Homarr selects the layout with the highest breakpoint that fits the current viewport. -Tiles use fixed `200 × 200` logical-pixel cells with a `24px` gap. Homarr scales the complete layout to the available -width instead of changing each widget's proportions. **Reset from Base** regenerates a Mobile or custom layout from the +The logical grid uses fixed `200 × 200` cells with a `12px` gap. Visible tile spacing also depends on card insets and +canvas scaling. Homarr scales the complete layout to the available width instead of changing each widget's proportions. **Reset from Base** regenerates a Mobile or custom layout from the current Base layout. Changing column or sidebar counts keeps a saved source arrangement while its generated positions remain unedited. From af4209886dc5e5eb399a3d1cfdc67825907ad647 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 3 Oct 2026 22:33:41 +0200 Subject: [PATCH 22/29] fix(board): persist setup checklist dismissal (#6992) --- apps/docs/docs/management/boards/index.mdx | 3 +++ .../board/board-setup-checklist.tsx | 25 +++++++++++++------ packages/translation/src/lang/en.json | 1 + 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/apps/docs/docs/management/boards/index.mdx b/apps/docs/docs/management/boards/index.mdx index effa13748..7d6267687 100644 --- a/apps/docs/docs/management/boards/index.mdx +++ b/apps/docs/docs/management/boards/index.mdx @@ -66,6 +66,9 @@ with enabled sidebars shown beside the main canvas. Enter edit mode from the board header to add, move, resize, configure, or remove content. Save the board to persist the layout. +Dismissing the setup checklist is remembered per board in the current browser, independently of board contents. +Clearing browser site data restores it; dismissal does not sync across browsers. + - Drag from an item's inactive content area; Containers use their grip. - Hold `Ctrl` on Windows/Linux or `Cmd` on macOS to select several items. - Use **Move / resize item** for another canvas, rail, Container, or exact coordinates. diff --git a/apps/nextjs/src/components/board/board-setup-checklist.tsx b/apps/nextjs/src/components/board/board-setup-checklist.tsx index 126cc24fd..b73987f34 100644 --- a/apps/nextjs/src/components/board/board-setup-checklist.tsx +++ b/apps/nextjs/src/components/board/board-setup-checklist.tsx @@ -2,7 +2,7 @@ import { ActionIcon, Affix, Button, Group, Paper, Progress, Stack, Text, ThemeIcon, Tooltip } from "@mantine/core"; import { useLocalStorage } from "@mantine/hooks"; -import { IconCheck, IconChevronDown, IconChecklist } from "@tabler/icons-react"; +import { IconCheck, IconChevronDown, IconChecklist, IconX } from "@tabler/icons-react"; import { useIntegrationsWithUseAccess } from "@homarr/auth/client"; import { useRequiredBoard } from "@homarr/boards/context"; @@ -22,6 +22,10 @@ export const BoardSetupChecklist = () => { key: `homarr-board-setup-checklist-${board.id}`, defaultValue: false, }); + const [dismissed, setDismissed] = useLocalStorage({ + key: `homarr-board-setup-checklist-dismissed-${board.id}`, + defaultValue: false, + }); const t = useI18n("board.setupChecklist"); const tCommon = useI18n("common.action"); const progress = getBoardSetupProgress({ @@ -29,7 +33,7 @@ export const BoardSetupChecklist = () => { usableIntegrationCount: integrations.length, }); - if (!isEditMode || progress.isComplete) return null; + if (!isEditMode || progress.isComplete || dismissed) return null; if (collapsed) { return ( @@ -58,11 +62,18 @@ export const BoardSetupChecklist = () => { {t("description")}
- - setCollapsed(true)}> - - - + + + setCollapsed(true)}> + + + + + setDismissed(true)}> + + + + Date: Sat, 3 Oct 2026 22:33:49 +0200 Subject: [PATCH 23/29] fix: include Jellyfin releases across mixed libraries (#6998) --- apps/docs/docs/integrations/jellyfin/index.mdx | 2 ++ packages/integrations/src/jellyfin/jellyfin-integration.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/apps/docs/docs/integrations/jellyfin/index.mdx b/apps/docs/docs/integrations/jellyfin/index.mdx index 83e23fd45..0fce0cc80 100644 --- a/apps/docs/docs/integrations/jellyfin/index.mdx +++ b/apps/docs/docs/integrations/jellyfin/index.mdx @@ -19,6 +19,8 @@ import { mediaReleasesWidget } from "@site/docs/widgets/media-releases"; +The Media Releases widget shows up to 100 recently added individual items across the libraries accessible to the Jellyfin user used by the integration. Episodes and music tracks appear individually instead of being grouped into seasons or albums. Jellyfin's library access and latest-media preferences still apply. + ### Adding the integration diff --git a/packages/integrations/src/jellyfin/jellyfin-integration.ts b/packages/integrations/src/jellyfin/jellyfin-integration.ts index ac44fdd19..7f6effdb6 100644 --- a/packages/integrations/src/jellyfin/jellyfin-integration.ts +++ b/packages/integrations/src/jellyfin/jellyfin-integration.ts @@ -236,6 +236,8 @@ export class JellyfinIntegration extends Integration implements IMediaServerInte fields: ["CustomRating", "Studios", "Genres", "ChildCount", "DateCreated", "Overview", "Taglines"], userId, limit: 100, + // Root-level grouping uses the first library's type and can omit items from other libraries. + groupItems: false, }); return result.data.map((item) => ({ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion From b40f000dd5381f48cf05d021e036e03566caa446 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 3 Oct 2026 22:33:57 +0200 Subject: [PATCH 24/29] fix(board): make container headers fully clickable (#7006) --- apps/docs/docs/management/boards/index.mdx | 3 +- .../board/sections/container-section.tsx | 33 ++++++++++++++----- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/apps/docs/docs/management/boards/index.mdx b/apps/docs/docs/management/boards/index.mdx index 7d6267687..b71d31c8e 100644 --- a/apps/docs/docs/management/boards/index.mdx +++ b/apps/docs/docs/management/boards/index.mdx @@ -50,7 +50,8 @@ was introduced; widen the affected Container before moving widgets into its righ ### Containers and rails A **Container** groups apps, widgets, and nested Containers. It can have a label, collapse control, app-opening action, -border color, and CSS classes. +border color, and CSS classes. Click anywhere on a collapsible Container’s header to collapse or expand it. +The header also supports Enter and Space; its app-opening action and edit controls work independently. Items can occupy the outermost rows and columns of a Container. The drop preview shows the position where the item will remain when released. diff --git a/apps/nextjs/src/components/board/sections/container-section.tsx b/apps/nextjs/src/components/board/sections/container-section.tsx index d18190329..9a93a9f32 100644 --- a/apps/nextjs/src/components/board/sections/container-section.tsx +++ b/apps/nextjs/src/components/board/sections/container-section.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext } from "react"; +import { createContext, useContext, useRef } from "react"; import dynamic from "next/dynamic"; import { ActionIcon, Badge, Box, Button, Card } from "@mantine/core"; import { IconChevronDown, IconChevronUp, IconExternalLink } from "@tabler/icons-react"; @@ -33,6 +33,7 @@ interface Props { export const BoardContainerSection = ({ section }: Props) => { const board = useRequiredBoard(); + const togglePointer = useRef<{ x: number; y: number; moved: boolean } | null>(null); const containerDepth = useContext(ContainerDepthContext); // Stagger nested controls inward and downward without wrapping back over ancestors. const menuRightOffset = 4 + containerDepth * 32; @@ -73,15 +74,15 @@ export const BoardContainerSection = ({ section }: Props) => { labelMaxWidth = `calc(100% - ${labelLeft}px - ${reservedMenuWidth})`; collapsedLabelPaddingRight = reservedMenuWidth; } - // Expanded controls sit on the border without reserving a header row. + // The native toggle spans the header; sibling actions stay above it. const toggleLayout = isVisuallyCollapsed ? { top: 0, left: 0, w: "100%", h: "100%", maw: "100%" } : { top: labelTop, - left: labelLeft, - w: "auto", + left: 0, + w: "100%", h: 20, - maw: labelMaxWidth, + maw: "100%", }; const toggleIcon = isVisuallyCollapsed ? ( @@ -125,13 +126,29 @@ export const BoardContainerSection = ({ section }: Props) => { pos="absolute" {...toggleLayout} px={6} - ps={isVisuallyCollapsed ? labelLeft : 6} - pe={isVisuallyCollapsed ? collapsedLabelPaddingRight : 6} + ps={labelLeft} + pe={collapsedLabelPaddingRight} radius="sm" variant="default" justify={options.showLabel ? "flex-start" : "center"} leftSection={options.showLabel && toggleIcon} - onClick={toggle} + onPointerDown={(event) => { + togglePointer.current = { x: event.clientX, y: event.clientY, moved: false }; + }} + onPointerMove={(event) => { + const pointer = togglePointer.current; + if (!pointer || event.buttons === 0) return; + if (Math.hypot(event.clientX - pointer.x, event.clientY - pointer.y) > 5) { + pointer.moved = true; + } + }} + onPointerCancel={() => { + togglePointer.current = null; + }} + onClick={(event) => { + if (event.detail > 0 && togglePointer.current?.moved) return; + toggle(); + }} aria-expanded={!isVisuallyCollapsed} aria-controls={contentId} aria-label={`${t(isVisuallyCollapsed ? "action.expand" : "action.collapse")}: ${label}`} From 83ae98847af68eef20a3b8622131697067c4eff6 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 3 Oct 2026 22:34:05 +0200 Subject: [PATCH 25/29] fix(onboarding): return to welcome before administrator creation (#7012) --- .../after-the-installation.mdx | 4 +- .../api/src/router/onboard/onboard-router.ts | 20 +++++++++- packages/onboarding/src/account.tsx | 37 +++++++++++++++++-- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/apps/docs/docs/getting-started/after-the-installation.mdx b/apps/docs/docs/getting-started/after-the-installation.mdx index 0115e9282..2d46873df 100644 --- a/apps/docs/docs/getting-started/after-the-installation.mdx +++ b/apps/docs/docs/getting-started/after-the-installation.mdx @@ -16,7 +16,9 @@ import { } from "@tabler/icons-react"; After Homarr starts, open its URL. An unfinished installation redirects to the onboarding screen at `/init`. Choose -**Get started** for a new installation or **Restore backup** to import a compatible SQLite backup. +**Get started** for a new installation or **Restore backup** to import a compatible SQLite backup. Before creating the +administrator, you can return to Welcome with **Back**, including after refreshing `/init`. Creating the administrator +commits this setup stage; later steps do not rewind it. Onboarding writes the administrator and board configuration to the database. If onboarding returns after a restart, diff --git a/packages/api/src/router/onboard/onboard-router.ts b/packages/api/src/router/onboard/onboard-router.ts index 2b6b0b73d..339ee4d30 100644 --- a/packages/api/src/router/onboard/onboard-router.ts +++ b/packages/api/src/router/onboard/onboard-router.ts @@ -8,7 +8,7 @@ import { createId } from "@homarr/common"; import { encryptSecret } from "@homarr/common/server"; import { createLogger } from "@homarr/core/infrastructure/logs"; import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; -import { handleTransactionsAsync, eq, inArray, like, or } from "@homarr/db"; +import { handleTransactionsAsync, and, eq, inArray, like, notExists, or } from "@homarr/db"; import { getServerSettingByKeyAsync } from "@homarr/db/queries"; import { apps, @@ -25,6 +25,7 @@ import { sectionLayouts, sections, serverSettings, + users, } from "@homarr/db/schema"; import { emptySuperJSON, @@ -147,6 +148,23 @@ export const onboardRouter = createTRPCRouter({ await nextOnboardingStepAsync(ctx.db); }), + returnToWelcome: onboardingProcedure.requiresStep("user").mutation(async ({ ctx }) => { + const returnedRows = await ctx.db + .update(onboarding) + .set({ step: "start", previousStep: null }) + .where( + and( + eq(onboarding.step, "user"), + eq(onboarding.previousStep, "start"), + notExists(ctx.db.select({ id: users.id }).from(users)), + ), + ) + .returning({ id: onboarding.id }); + if (returnedRows.length !== 1) { + throw new TRPCError({ code: "PRECONDITION_FAILED", message: "Administrator setup can no longer be restarted." }); + } + }), + testIntegration: onboardingProcedure .requiresStep("setup") .input(onboardingIntegrationDraftSchema) diff --git a/packages/onboarding/src/account.tsx b/packages/onboarding/src/account.tsx index 0b084d168..9169c797b 100644 --- a/packages/onboarding/src/account.tsx +++ b/packages/onboarding/src/account.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react"; import { Alert, Button, Stack, Text, TextInput } from "@mantine/core"; -import { IconArrowRight, IconShieldCheck, IconUserPlus } from "@tabler/icons-react"; +import { IconArrowLeft, IconArrowRight, IconShieldCheck, IconUserPlus } from "@tabler/icons-react"; import type { z } from "zod/v4"; import { clientApi } from "@homarr/api/client"; @@ -36,11 +36,12 @@ const AccountShell = ({ ); export const AccountSetup = ({ environment }: OnboardingStudioProps) => { - if (environment.currentStep === "user") return ; + if (environment.currentStep === "user") return ; return ; }; -const CredentialsSetup = () => { +const CredentialsSetup = ({ canReturnToWelcome }: { canReturnToWelcome: boolean }) => { + const navigationT = useI18n("init.studio"); const t = useI18n("init.studio.account"); const [requiresSignIn, setRequiresSignIn] = useState(false); const [submitError, setSubmitError] = useState(null); @@ -54,6 +55,16 @@ const CredentialsSetup = () => { showErrorNotification({ title: t("errorTitle"), message }); }, }); + const returnToWelcome = clientApi.onboard.returnToWelcome.useMutation({ + async onSuccess() { + sounds.swoosh(); + await revalidatePathActionAsync("/init"); + }, + onError() { + sounds.error(); + showErrorNotification({ title: t("errorTitle"), message: t("unknownError") }); + }, + }); const form = useZodForm(userInitSchema, { initialValues: { username: "", password: "", confirmPassword: "" }, }); @@ -119,9 +130,27 @@ const CredentialsSetup = () => { {submitError} ) : null} - + {canReturnToWelcome && ( + + )}
From c1dd2b3bcad32e2a7fc67168bd45d06f43b47a3e Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 3 Oct 2026 23:14:37 +0200 Subject: [PATCH 26/29] fix(unifi): count active remote VPN sessions (#7013) --- .../integrations/unifi-controller/index.mdx | 6 + apps/docs/public/api/open-api-schema.json | 128 ++++++++++++++++++ .../network-controller-summary-types.ts | 2 +- .../unifi-controller-integration.ts | 47 ++++++- .../network-controller/summary/component.tsx | 17 +-- .../src/network-controller/summary/display.ts | 2 +- 6 files changed, 190 insertions(+), 12 deletions(-) diff --git a/apps/docs/docs/integrations/unifi-controller/index.mdx b/apps/docs/docs/integrations/unifi-controller/index.mdx index 7d9666cb5..7a1a8869b 100644 --- a/apps/docs/docs/integrations/unifi-controller/index.mdx +++ b/apps/docs/docs/integrations/unifi-controller/index.mdx @@ -24,6 +24,12 @@ Therefore it should support CloudKey Gen1, CloudKey Gen2, UnifiOS-based UDM-Pro items={[{ widget: networkControllerSummaryWidget }, { widget: networkControllerStatusWidget }]} /> +VPN users are counted from active remote VPN sessions across all sites. Controllers that return HTTP 404 for +`stat/remoteuservpn` use the legacy health counter instead; a successful empty session list counts as zero. +This avoids the stale L2TP health counter on UniFi Network 10.6.106. VPN types absent from that endpoint cannot be +counted. If a site's session request fails or returns an invalid response, the VPN user total is shown as unknown +while the other network metrics remain available. VPN status still uses the controller's health subsystem. + ### Adding the integration diff --git a/apps/docs/public/api/open-api-schema.json b/apps/docs/public/api/open-api-schema.json index d6d5ce027..30129299f 100644 --- a/apps/docs/public/api/open-api-schema.json +++ b/apps/docs/public/api/open-api-schema.json @@ -5923,6 +5923,134 @@ } } } + }, + "/api/integrations/{id}/test-connection": { + "get": { + "operationId": "integration-testConnection", + "summary": "Test a saved integration using stored credentials", + "description": "Requires full integration access. Runs the native connection check without changing saved configuration. Upstream authentication may establish a session. Success is a connection check, not widget or service acceptance. Failures expose only a safe category, not upstream errors or credentials.", + "tags": [ + "integrations" + ], + "security": [ + { + "apikey": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "const": true + } + }, + "required": [ + "success" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "const": false + }, + "error": { + "type": "string", + "enum": [ + "missingSecret", + "authorization", + "certificate", + "request", + "statusCode", + "parse", + "unknown" + ] + } + }, + "required": [ + "success", + "error" + ], + "additionalProperties": false + } + ], + "type": "object" + } + } + } + }, + "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": { diff --git a/packages/integrations/src/interfaces/network-controller-summary/network-controller-summary-types.ts b/packages/integrations/src/interfaces/network-controller-summary/network-controller-summary-types.ts index 587b5470d..fb2a30ac9 100644 --- a/packages/integrations/src/interfaces/network-controller-summary/network-controller-summary-types.ts +++ b/packages/integrations/src/interfaces/network-controller-summary/network-controller-summary-types.ts @@ -22,6 +22,6 @@ export interface NetworkControllerSummary { vpn: { status: "enabled" | "disabled"; - users: number; + users: number | null; }; } diff --git a/packages/integrations/src/unifi-controller/unifi-controller-integration.ts b/packages/integrations/src/unifi-controller/unifi-controller-integration.ts index 2a5be3b8c..20c23ca0d 100644 --- a/packages/integrations/src/unifi-controller/unifi-controller-integration.ts +++ b/packages/integrations/src/unifi-controller/unifi-controller-integration.ts @@ -1,6 +1,7 @@ import type tls from "node:tls"; -import axios from "axios"; +import axios, { isAxiosError } from "axios"; import { HttpCookieAgent, HttpsCookieAgent } from "http-cookie-agent/http"; +import { z } from "zod/v4"; import { getAllTrustedCertificatesAsync, @@ -11,6 +12,7 @@ import { fetchWithTrustedCertificatesAsync, getHttpRequestSignal, } from "@homarr/core/infrastructure/http"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import type { SiteStats } from "@homarr/node-unifi"; import Unifi from "@homarr/node-unifi"; @@ -25,6 +27,10 @@ import type { NetworkControllerSummaryIntegration } from "../interfaces/network- import type { NetworkControllerSummary } from "../interfaces/network-controller-summary/network-controller-summary-types"; import type { HealthSubsystem } from "./unifi-controller-types"; +const logger = createLogger({ module: "unifi-controller-integration" }); + +const remoteVpnSessionsSchema = z.array(z.object({ up: z.boolean() })); + @HandleIntegrationErrors([integrationAxiosHttpErrorHandler]) export class UnifiControllerIntegration extends Integration implements NetworkControllerSummaryIntegration { private readonly httpSessionStore = createSessionStore<{ cookie: string; csrfToken?: string }>(this.integration); @@ -45,6 +51,43 @@ export class UnifiControllerIntegration extends Integration implements NetworkCo public async getNetworkSummaryAsync(): Promise { const client = await this.createControllerClientAsync(); const stats = await client.getSitesStats(); + const vpnUserCounts = await Promise.all( + stats.map(async (site) => { + try { + const sessions: unknown = await client.customApiRequest( + `/api/s/${encodeURIComponent(site.name)}/stat/remoteuservpn`, + ); + return remoteVpnSessionsSchema.parse(sessions).filter((session) => session.up).length; + } catch (error) { + // Older controllers may not expose remote sessions. An empty successful + // response is authoritative; only an unsupported endpoint uses health. + if (isAxiosError(error) && error.response?.status === 404) { + return this.getNumericValueOverAllSites([site], "vpn", (vpn) => vpn.remote_user_num_active, "sum"); + } + let status: number | undefined; + if (isAxiosError(error)) status = error.response?.status; + let reason = "request-failed"; + if (error instanceof z.ZodError) reason = "invalid-response"; + logger.warn("UniFi remote VPN session count unavailable", { + integrationId: this.integration.id, + site: site.name, + status, + reason, + }); + return null; + } + }), + ); + + // A partial site total cannot describe the controller's connected users. + let vpnUsers: number | null = 0; + for (const count of vpnUserCounts) { + if (count === null) { + vpnUsers = null; + break; + } + vpnUsers += count; + } return { wanStatus: this.getStatusValueOverAllSites(stats, "wan", (site) => site.status === "ok"), @@ -66,7 +109,7 @@ export class UnifiControllerIntegration extends Integration implements NetworkCo }, vpn: { status: this.getStatusValueOverAllSites(stats, "vpn", (site) => site.status === "ok"), - users: this.getNumericValueOverAllSites(stats, "vpn", (site) => site.remote_user_num_active, "sum"), + users: vpnUsers, }, } satisfies NetworkControllerSummary; } diff --git a/packages/widgets/src/network-controller/summary/component.tsx b/packages/widgets/src/network-controller/summary/component.tsx index d0e8b3a3d..377508e49 100644 --- a/packages/widgets/src/network-controller/summary/component.tsx +++ b/packages/widgets/src/network-controller/summary/component.tsx @@ -66,6 +66,10 @@ export default function NetworkControllerSummaryWidget({ ); const firstSummary = summaries[0]; + const formatVpnCount = (count: number | null) => { + if (count === null) return statusLabels.unknown; + return t("card.vpn.countConnected", { count }); + }; let responsiveWidth = width; let responsiveHeight = height; @@ -154,15 +158,11 @@ export default function NetworkControllerSummaryWidget({ /> } > - + {t("card.vpn.label")} {showSecondary && ( - {t("card.vpn.countConnected", { count: summary.vpn.users })} + {formatVpnCount(summary.vpn.users)} )} @@ -234,7 +234,7 @@ export default function NetworkControllerSummaryWidget({ {getMatrixMetricLabel(metric.key, t)} - {formatMatrixMetric(metric.key, metric.value)} + {formatMatrixMetric(metric.key, metric.value, statusLabels.unknown)}
))} @@ -257,7 +257,8 @@ const getMatrixMetricLabel = ( t: ScopedTranslationFunction<"widget.networkControllerSummary">, ) => t(`advanced.metric.${key}` as never); -const formatMatrixMetric = (key: NetworkControllerMatrixMetricKey, value: number) => { +const formatMatrixMetric = (key: NetworkControllerMatrixMetricKey, value: number | null, unknown: string) => { + if (value === null) return unknown; if (key === "latency" || key === "ping") return `${value}ms`; if (key === "uptime") return dayjs.duration(value, "seconds").humanize(); return String(value); diff --git a/packages/widgets/src/network-controller/summary/display.ts b/packages/widgets/src/network-controller/summary/display.ts index 11cb28c29..035490f50 100644 --- a/packages/widgets/src/network-controller/summary/display.ts +++ b/packages/widgets/src/network-controller/summary/display.ts @@ -6,7 +6,7 @@ export type NetworkControllerMatrixMetricKey = "latency" | "ping" | "uptime" | " export interface NetworkControllerMatrixSection { key: NetworkControllerMatrixSectionKey; status: "enabled" | "disabled"; - metrics: { key: NetworkControllerMatrixMetricKey; value: number }[]; + metrics: { key: NetworkControllerMatrixMetricKey; value: number | null }[]; } export const getNetworkControllerMatrix = (summary: NetworkControllerSummary): NetworkControllerMatrixSection[] => [ From d15987c9fbf937a18e0b12ac322c3230dd200c99 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 3 Oct 2026 23:14:54 +0200 Subject: [PATCH 27/29] fix(docker): resolve Homepage Dashboard Icons keys (#7004) --- .../docs/integrations/docker-labels/index.mdx | 9 +++++++++ .../discovery/parse-container-labels.spec.ts | 2 +- .../src/discovery/parse-container-labels.ts | 17 ++++++++++++++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/apps/docs/docs/integrations/docker-labels/index.mdx b/apps/docs/docs/integrations/docker-labels/index.mdx index 377116020..d5ed8990b 100644 --- a/apps/docs/docs/integrations/docker-labels/index.mdx +++ b/apps/docs/docs/integrations/docker-labels/index.mdx @@ -67,6 +67,15 @@ When `homarr.name` is absent, onboarding also understands these Homepage labels: `homepage.name` and `homepage.href` are required for the fallback. Homarr does not combine partial Homarr metadata with Homepage metadata: once `homarr.name` is present, use `homarr.*` labels for the remaining fields too. +Homepage Dashboard Icons keys such as `sonarr` or `jellyfin.svg` are converted to full icon URLs. Keys are lowercased +to match published filenames. Bare keys use PNG; +`.png`, `.svg`, and `.webp` select the corresponding format. Explicit URLs and local paths are preserved. Homepage's +`mdi-`, `si-`, and `sh-` icon prefixes are not converted; use a full icon URL for those sets. Local paths must also be +available on the Homarr server. `homarr.icon` continues to accept an icon URL. + +This applies to new onboarding imports. For apps already imported with broken icons, change their icons in app +management. + ## What onboarding creates In **Connect**, label-discovered apps and integrations appear before image-based suggestions. You can import multiple diff --git a/packages/docker/src/discovery/parse-container-labels.spec.ts b/packages/docker/src/discovery/parse-container-labels.spec.ts index 22f4dbd2b..69b290375 100644 --- a/packages/docker/src/discovery/parse-container-labels.spec.ts +++ b/packages/docker/src/discovery/parse-container-labels.spec.ts @@ -55,7 +55,7 @@ describe("parseContainerLabels", () => { name: "Jellyfin", group: "Media", href: "http://jellyfin:8096", - icon: "jellyfin.svg", + icon: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/jellyfin.svg", }); }); diff --git a/packages/docker/src/discovery/parse-container-labels.ts b/packages/docker/src/discovery/parse-container-labels.ts index 56510edbd..0fbba478f 100644 --- a/packages/docker/src/discovery/parse-container-labels.ts +++ b/packages/docker/src/discovery/parse-container-labels.ts @@ -25,6 +25,21 @@ const resolveLabelValue = ( useHomepageFallback: boolean, ) => readLabel(labels, homarrKey) ?? (useHomepageFallback ? readLabel(labels, homepageKey) : undefined); +const resolveIcon = (labels: Record, useHomepageFallback: boolean) => { + const homarrIcon = readLabel(labels, dockerLabels.icon); + if (homarrIcon || !useHomepageFallback) return homarrIcon; + + const homepageIcon = readLabel(labels, homepageLabels.icon); + if (!homepageIcon || /^(mdi|si|sh)-/.test(homepageIcon)) return homepageIcon; + + // Homepage's Dashboard Icons keys default to PNG; URLs and local paths pass through. + const dashboardIcon = /^([a-zA-Z0-9][a-zA-Z0-9_-]*)(?:\.(png|svg|webp))?$/.exec(homepageIcon); + if (!dashboardIcon?.[1]) return homepageIcon; + + const extension = dashboardIcon[2] ?? "png"; + return `https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/${extension}/${dashboardIcon[1].toLowerCase()}.${extension}`; +}; + const parseIntegrationKind = (value: string | undefined): IntegrationKind | undefined => value && integrationKindSet.has(value) ? (value as IntegrationKind) : undefined; @@ -57,7 +72,7 @@ export const parseContainerLabels = ( group, name, href, - icon: resolveLabelValue(labels, dockerLabels.icon, homepageLabels.icon, useHomepageFallback), + icon: resolveIcon(labels, useHomepageFallback), description: resolveLabelValue(labels, dockerLabels.description, homepageLabels.description, useHomepageFallback), pingUrl: readLabel(labels, dockerLabels.ping), externalId, From 5a2f6cfa1bcf27f96d4f17aef6985bb81d597c97 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 3 Oct 2026 23:31:11 +0200 Subject: [PATCH 28/29] docs: require agent docs and tests only when user-critical (#7021) --- .agents/skills/codebase-context/SKILL.md | 2 +- .agents/skills/documentation-sync/SKILL.md | 6 +++--- .agents/skills/documentation-sync/agents/openai.yaml | 4 ++-- .github/pull_request_template.md | 2 +- AGENTS.md | 10 +++++----- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.agents/skills/codebase-context/SKILL.md b/.agents/skills/codebase-context/SKILL.md index 4f24645b8..d7676ad92 100644 --- a/.agents/skills/codebase-context/SKILL.md +++ b/.agents/skills/codebase-context/SKILL.md @@ -37,4 +37,4 @@ Orient from the current checkout before editing. Treat package manifests, export - Cron jobs: `packages/cron-jobs/src/jobs/` - App routes: `apps/nextjs/src/app/[locale]/` -Invoke `documentation-sync` when hidden capabilities or information not evident to an advanced user change, or existing guidance becomes inaccurate; follow the Documentation Sync criteria in `AGENTS.md`. When exposing tRPC through MCP, invoke `mcp-integration`. +Invoke `documentation-sync` only when users need new or corrected information to set up, use, or troubleshoot Homarr; follow the Documentation Sync criteria in `AGENTS.md`. When exposing tRPC through MCP, invoke `mcp-integration`. diff --git a/.agents/skills/documentation-sync/SKILL.md b/.agents/skills/documentation-sync/SKILL.md index 0f06a2f15..ecf88992c 100644 --- a/.agents/skills/documentation-sync/SKILL.md +++ b/.agents/skills/documentation-sync/SKILL.md @@ -1,11 +1,11 @@ --- name: documentation-sync -description: Write concise Homarr docs only when changed behavior is not clear to a smart, advanced user from the UI or generated schema. Skip routine, obvious, or self-explanatory changes. +description: Write Homarr docs only when users need information to set up, use, or troubleshoot the app. Skip routine, obvious, or self-explanatory changes. --- # Documentation Sync -Apply the audience and scope rules in `AGENTS.md` first. A code, UI, or API change alone does not warrant documentation. Add a passage only when a smart, advanced user cannot infer the behavior from the interface or generated schema. +Apply the audience and scope rules in `AGENTS.md` first. Documentation exists only for information users need to set up, use, or troubleshoot Homarr. A code, UI, or API change alone does not warrant documentation; if a user does not need to know it, do not add it. For hidden capabilities such as the advanced widget feature, explain discovery, activation, and non-obvious behavior. For visible standard controls such as Delete, omit narration of what the label already conveys. Use the shortest explanation that preserves necessary technical detail; remove redundant prose in the passage being edited. @@ -50,7 +50,7 @@ For a widget: ## Completion criteria -1. Every passage adds information an advanced user cannot reasonably infer from the interface. Remove obvious, repetitive, or unnecessary prose. +1. Every passage adds information a user needs and cannot reasonably infer from the interface. Remove obvious, repetitive, or unnecessary prose. 2. Keep names, defaults, paths, screenshots, links, and prerequisites consistent with code. 3. Update every affected page and remove superseded guidance. 4. Run the narrowest useful docs validation. Use `pnpm turbo build --filter=@homarr/docs` when links, MDX, generated definitions, or navigation can fail; otherwise run the docs package formatter on touched files. diff --git a/.agents/skills/documentation-sync/agents/openai.yaml b/.agents/skills/documentation-sync/agents/openai.yaml index 9c9b77cc5..4cb45e135 100644 --- a/.agents/skills/documentation-sync/agents/openai.yaml +++ b/.agents/skills/documentation-sync/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Homarr Documentation Sync" - short_description: "Keep user-facing docs aligned with code" - default_prompt: "Use $documentation-sync to map this Homarr code change to the required documentation updates." + short_description: "Document only what users need to know" + default_prompt: "Use $documentation-sync only when users need new or corrected information to set up, use, or troubleshoot Homarr." diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 236fd1787..dbaab3791 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -10,7 +10,7 @@ - [ ] Pull request targets the intended base branch (`dev` by default; `release/v2` for v2 release work) - [ ] Commits follow the [conventional commits guideline](https://www.conventionalcommits.org/en/v1.0.0/) - [ ] No shorthand variable names are used (for example, `x`, `y`, `i`, or abbreviations) -- [ ] User-facing changes include matching documentation updates under `apps/docs` +- [ ] Documentation under `apps/docs` is updated only when users need new or corrected information to use the app - [ ] When using AI, no temporary files are checked in and the code style follows the rest of the project **When contributing a new integration or widget:** diff --git a/AGENTS.md b/AGENTS.md index 48b2acadd..aa3b9efa8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,11 +57,11 @@ homarr/ ## Documentation -Add or update docs only when a smart, advanced user cannot infer the changed behavior from the UI or generated API schema. Document hidden capabilities, surprising behavior, non-obvious prerequisites or constraints, configuration contracts, and migrations. A code or API change alone is not a reason to add docs. +Add documentation only when users need the information to set up, use, or troubleshoot Homarr and cannot reasonably discover it from the UI or generated API schema. If a user does not need to know it, do not document it. A code, UI, or API change alone is never a reason to add docs. -Keep warranted documentation concise. Omit UI walkthroughs, visible control descriptions, and details already clear from the interface or schema. +Keep warranted documentation concise. Omit UI walkthroughs, visible control descriptions, internal implementation details, and anything already clear from the interface or schema. -Only after this reader-value test passes, use these locations: +Only after this user-need test passes, use these locations: - New integration → `apps/docs/docs/integrations//index.mdx` + `index.ts` - New widget → `apps/docs/docs/widgets//index.mdx` + `index.ts` @@ -97,7 +97,7 @@ Only after this reader-value test passes, use these locations: ## Testing -Add tests only when requested. Favor assertions that would fail for a plausible regression in user-visible behavior or a security boundary; avoid checks that repeat implementation details or duplicate stronger coverage. For focused changes, run only the relevant existing or newly requested checks when validation is needed. Do not run broad test, Docker, or E2E suites by default. +Add tests only when requested, and only when the test is extremely relevant and will plausibly prevent a future regression in user-visible behavior or a security boundary. Do not add tests that restate implementation details or duplicate stronger coverage. For focused changes, run only the relevant existing or newly requested checks when validation is needed. Do not run broad test, Docker, or E2E suites by default. ## MCP servers @@ -108,6 +108,6 @@ Add tests only when requested. Favor assertions that would fail for a plausible Portable skills live in `.agents/skills/`. Read the relevant `SKILL.md` before working in that domain; detailed references are loaded only when needed. Claude-compatible discovery is provided through `.claude/skills`. - `codebase-context` — architecture, package boundaries, and shared utilities -- `documentation-sync` — documentation for changes users need explained +- `documentation-sync` — user-critical documentation only - `mcp-integration` — safe tRPC-to-MCP exposure - `homarr-custom-widget` — safe Custom JSX v2 authoring From f50d4ed134eff15689c2642abbbd8a9aec9bf7df Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 3 Oct 2026 23:33:23 +0200 Subject: [PATCH 29/29] fix(integrations): authenticate Frigate stats and HTTP requests (#7019) * fix(integrations): authenticate Frigate stats and HTTP requests [skip ci] * fix(integrations): reject HTTP Frigate URLs before authentication --- apps/docs/docs/integrations/frigate/index.mdx | 4 +- packages/definitions/src/integration.ts | 4 +- .../src/stats/frigate-auth.spec.ts | 205 ++++++++++++++++++ .../src/stats/providers/frigate.ts | 50 ++++- .../src/stats/stats-integration.ts | 27 +++ packages/integrations/src/stats/types.ts | 6 +- 6 files changed, 288 insertions(+), 8 deletions(-) create mode 100644 packages/integrations/src/stats/frigate-auth.spec.ts diff --git a/apps/docs/docs/integrations/frigate/index.mdx b/apps/docs/docs/integrations/frigate/index.mdx index 2c91cb12d..f41d82e1e 100644 --- a/apps/docs/docs/integrations/frigate/index.mdx +++ b/apps/docs/docs/integrations/frigate/index.mdx @@ -22,4 +22,6 @@ The Frigate statistics use `/api/stats` and report camera count, service uptime, -Use Frigate's internal HTTP API (port **5000**), reachable from Homarr. This endpoint does not require credentials; the authenticated web interface on port 8971 is not supported by this integration. Keep the internal API on a trusted network. +Use Frigate's authenticated interface (port **8971**) with an admin account's username and password. Homarr exchanges these for a session token and verifies the account before requesting statistics. Frigate's default `frigate_token` cookie name is required. The integration URL must use `https://`; Homarr rejects `http://` URLs so credentials are never sent unencrypted, and certificates must be trusted by Homarr. + +Existing integrations using the unauthenticated port **5000** must update their URL and add credentials. Homarr does not fall back to the unauthenticated API; keep that listener private. diff --git a/packages/definitions/src/integration.ts b/packages/definitions/src/integration.ts index 027cdd35f..2d0c5df2c 100644 --- a/packages/definitions/src/integration.ts +++ b/packages/definitions/src/integration.ts @@ -119,11 +119,11 @@ export const integrationDefs = { frigate: { httpAuth: { type: "adapter" }, name: "Frigate", - secretKinds: [[]], + secretKinds: [["username", "password"]], iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/frigate.svg", category: ["miscellaneous"], documentationSlug: "frigate", - defaultPort: 5000, + defaultPort: 8971, }, komga: { httpAuth: { type: "apiKeyHeader", name: "X-API-Key" }, diff --git a/packages/integrations/src/stats/frigate-auth.spec.ts b/packages/integrations/src/stats/frigate-auth.spec.ts new file mode 100644 index 000000000..d0c792236 --- /dev/null +++ b/packages/integrations/src/stats/frigate-auth.spec.ts @@ -0,0 +1,205 @@ +// @vitest-environment node + +import { inspect } from "node:util"; + +import { Headers, Response } from "undici"; +import { beforeEach, describe, expect, test, vi } from "vitest"; + +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; +import { integrationDefs } from "@homarr/definitions"; + +import type { IntegrationInput, IntegrationTestingInput } from "../base/integration"; +import { frigateStatsProvider } from "./providers/frigate"; +import { StatsIntegration } from "./stats-integration"; + +const logger = vi.hoisted(() => ({ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() })); + +vi.mock("@homarr/core/infrastructure/http", () => ({ + fetchWithTrustedCertificatesAsync: vi.fn(), + createAxiosCertificateInstanceAsync: vi.fn(), + createCertificateAgentAsync: vi.fn(), +})); +vi.mock("@homarr/core/infrastructure/certificates", () => ({ + getTrustedCertificateHostnamesAsync: vi.fn().mockResolvedValue([]), +})); +vi.mock("@homarr/core/infrastructure/logs", () => ({ + createLogger: () => logger, +})); + +const token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiJ9.test_signature"; +const credentials = [ + { kind: "username" as const, value: "operator" }, + { kind: "password" as const, value: "private-test-password" }, +]; +const fetchAsync = vi.mocked(fetchWithTrustedCertificatesAsync); +const createIntegration = ( + decryptedSecrets: IntegrationInput["decryptedSecrets"] = credentials, + url = "https://frigate.test:8971", +) => + new StatsIntegration( + { + id: "frigate-test", + kind: "frigate", + name: "Frigate", + url, + externalUrl: null, + decryptedSecrets, + }, + frigateStatsProvider, + ); + +const loginResponse = (cookie = `frigate_token=${token}; HttpOnly; Path=/; SameSite=Lax`) => + new Response(null, { status: 200, headers: { "set-cookie": cookie } }); +const profileResponse = (username = "operator", role = "admin") => + Response.json({ username, role, allowed_cameras: [] }); +const statsResponse = () => Response.json({ cameras: { front: {} }, service: { uptime: 42, version: "0.18.0" } }); + +beforeEach(() => fetchAsync.mockReset()); + +describe("authenticated Frigate", () => { + test("uses a bodyless cookie login and the same verified session for native stats", async () => { + fetchAsync + .mockResolvedValueOnce(loginResponse()) + .mockResolvedValueOnce(profileResponse()) + .mockResolvedValueOnce(statsResponse()); + expect(await createIntegration().getStatsAsync()).toEqual({ cameras: 1, uptime: 42, version: "0.18.0" }); + expect(fetchAsync.mock.calls.map(([url]) => String(url))).toEqual([ + "https://frigate.test:8971/api/login", + "https://frigate.test:8971/api/profile", + "https://frigate.test:8971/api/stats", + ]); + const login = fetchAsync.mock.calls[0]?.[1]; + expect(login).toMatchObject({ + method: "POST", + body: JSON.stringify({ user: "operator", password: credentials[1]?.value }), + redirect: "error", + }); + expect(new Headers(login?.headers).get("origin")).toBeNull(); + expect(login?.signal).toBeInstanceOf(AbortSignal); + for (const [, init] of fetchAsync.mock.calls.slice(1)) { + expect(new Headers(init?.headers).get("authorization")).toBe(`Bearer ${token}`); + expect(init?.redirect).toBe("error"); + } + }); + + test("supplies verified bearer credentials and explicit token redaction for generic requests", async () => { + fetchAsync.mockResolvedValueOnce(loginResponse()).mockResolvedValueOnce(profileResponse()); + expect(await createIntegration().getHttpAuthenticationAsync()).toEqual({ + headers: { Authorization: `Bearer ${token}` }, + redactValues: [token], + }); + expect(fetchAsync).toHaveBeenCalledTimes(2); + }); + + test("uses the injected trusted transport for native connectivity testing", async () => { + const injectedFetch = vi + .fn() + .mockResolvedValueOnce(loginResponse()) + .mockResolvedValueOnce(profileResponse()) + .mockResolvedValueOnce(statsResponse()); + // Only fetch is used by this provider; the other transports must never be invoked. + const testing = { fetchAsync: injectedFetch } as unknown as IntegrationTestingInput; + expect(await createIntegration().getStatsAsync(AbortSignal.timeout(1000), testing)).toMatchObject({ uptime: 42 }); + expect(injectedFetch).toHaveBeenCalledTimes(3); + expect(fetchAsync).not.toHaveBeenCalled(); + }); + + test.each([ + { secrets: [] }, + { secrets: [{ kind: "username" as const, value: "operator" }] }, + { + secrets: [ + { kind: "username" as const, value: "" }, + { kind: "password" as const, value: "private-test-password" }, + ], + }, + ])("rejects missing credentials without attempting an unauthenticated request (%j)", async ({ secrets }) => { + await expect(createIntegration(secrets).getStatsAsync()).rejects.toThrow(); + expect(fetchAsync).not.toHaveBeenCalled(); + }); + + test.each([401, 403, 404, 302])("rejects login status %i before profile or stats", async (status) => { + fetchAsync.mockResolvedValueOnce(new Response("private login error", { status })); + await expect(createIntegration().getStatsAsync()).rejects.toThrow(); + expect(fetchAsync).toHaveBeenCalledTimes(1); + }); + + test.each(["other_cookie=value", "frigate_token=not-a-jwt", `frigate_token=${"x".repeat(4097)}.b.c`])( + "rejects missing or invalid cookies", + async (cookie) => { + fetchAsync.mockResolvedValueOnce(loginResponse(cookie)); + await expect(createIntegration().getHttpAuthenticationAsync()).rejects.toThrow(); + expect(fetchAsync).toHaveBeenCalledTimes(1); + }, + ); + + test("rejects duplicate session cookies", async () => { + const headers = new Headers(); + headers.append("set-cookie", `frigate_token=${token}; Path=/`); + headers.append("set-cookie", `frigate_token=${token}; Path=/api`); + fetchAsync.mockResolvedValueOnce(new Response(null, { headers })); + await expect(createIntegration().getStatsAsync()).rejects.toThrow(); + expect(fetchAsync).toHaveBeenCalledTimes(1); + }); + + test.each([ + ["operator", "viewer"], + ["another-admin", "admin"], + ])("rejects a profile for %s with role %s before stats", async (username, role) => { + fetchAsync.mockResolvedValueOnce(loginResponse()).mockResolvedValueOnce(profileResponse(username, role)); + await expect(createIntegration().getStatsAsync()).rejects.toThrow(); + expect(fetchAsync).toHaveBeenCalledTimes(2); + }); + + test("bounds private response headers and discards the login body without parsing it", async () => { + const response = new Response("not JSON", { headers: { "set-cookie": `other=${"x".repeat(16_384)}` } }); + fetchAsync.mockResolvedValueOnce(response); + await expect(createIntegration().getHttpAuthenticationAsync()).rejects.toThrow(); + expect(response.body?.locked).toBe(false); + expect(fetchAsync).toHaveBeenCalledTimes(1); + }); + + test("discards a successful login body without parsing or exposing its contents", async () => { + const response = new Response(`private body ${"x".repeat(65_536)}`, { + headers: { "set-cookie": `frigate_token=${token}; Path=/` }, + }); + const parse = vi.spyOn(response, "json"); + if (!response.body) throw new Error("Fixture must have a body"); + const cancel = vi.spyOn(response.body, "cancel"); + fetchAsync.mockResolvedValueOnce(response).mockResolvedValueOnce(profileResponse()); + expect(await createIntegration().getHttpAuthenticationAsync()).toMatchObject({ redactValues: [token] }); + expect(cancel).toHaveBeenCalledOnce(); + expect(parse).not.toHaveBeenCalled(); + }); + + test.each(["login", "profile", "stats"])("does not expose transport secrets from %s", async (stage) => { + if (stage !== "login") fetchAsync.mockResolvedValueOnce(loginResponse()); + if (stage === "stats") fetchAsync.mockResolvedValueOnce(profileResponse()); + fetchAsync.mockRejectedValueOnce( + new Error(`secret ${credentials[1]?.value} ${token}`, { cause: new Error(token) }), + ); + const failure: unknown = await createIntegration() + .getStatsAsync() + .catch((error: unknown) => error); + expect(String(failure)).not.toContain(token); + expect(String(failure)).not.toContain(credentials[1]?.value); + expect(JSON.stringify(failure)).not.toContain(token); + expect(JSON.stringify(failure)).not.toContain(credentials[1]?.value); + expect(inspect(failure, { depth: 10 })).not.toContain(token); + expect(inspect(failure, { depth: 10 })).not.toContain(credentials[1]?.value); + expect(inspect(logger.warn.mock.calls, { depth: 10 })).not.toContain(token); + expect(inspect(logger.warn.mock.calls, { depth: 10 })).not.toContain(credentials[1]?.value); + }); + + test("rejects HTTP integration URLs without transmitting credentials", async () => { + const integration = createIntegration(credentials, "http://frigate.test:8971"); + await expect(integration.getHttpAuthenticationAsync()).rejects.toThrow(); + await expect(integration.getStatsAsync()).rejects.toThrow(); + expect(fetchAsync).not.toHaveBeenCalled(); + }); + + test("requires saved credentials and defaults to the authenticated port", () => { + expect(integrationDefs.frigate.secretKinds).toEqual([["username", "password"]]); + expect(integrationDefs.frigate.defaultPort).toBe(8971); + }); +}); diff --git a/packages/integrations/src/stats/providers/frigate.ts b/packages/integrations/src/stats/providers/frigate.ts index a742eb991..aa5428004 100644 --- a/packages/integrations/src/stats/providers/frigate.ts +++ b/packages/integrations/src/stats/providers/frigate.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import type { StatsProvider } from "../types"; +import type { StatsFetchContext, StatsProvider } from "../types"; const statsSchema = z .object({ @@ -10,14 +10,56 @@ const statsSchema = z .passthrough(); export const frigateStatsProvider = { - getHttpAuthentication: () => ({ headers: {} }), + getHttpAuthenticationAsync, metrics: [ { key: "cameras", label: "Cameras", unit: "count" }, { key: "uptime", label: "Uptime", unit: "seconds" }, { key: "version", label: "Version", unit: "text" }, ], async fetchAsync(context) { - const stats = statsSchema.parse(await context.requestAsync("/api/stats", { signal: context.signal })); - return { cameras: Object.keys(stats.cameras).length, uptime: stats.service.uptime, version: stats.service.version }; + const authentication = await getHttpAuthenticationAsync(context); + try { + const stats = statsSchema.parse( + await context.requestAsync("/api/stats", { headers: authentication.headers, signal: context.signal }), + ); + return { + cameras: Object.keys(stats.cameras).length, + uptime: stats.service.uptime, + version: stats.service.version, + }; + } catch { + throw new Error("Frigate stats request failed"); + } }, } satisfies StatsProvider; + +async function getHttpAuthenticationAsync(context: StatsFetchContext) { + if (context.url.protocol !== "https:") { + throw new Error("Frigate authentication requires an HTTPS integration URL to protect credentials"); + } + try { + const username = context.secret("username"); + const password = context.secret("password"); + if (!username || !password) throw new Error("Missing credentials"); + const response = await context.requestResponseAsync("/api/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ user: username, password }), + signal: context.signal, + }); + if (response.status !== 200) throw new Error("Login rejected"); + const cookies = response.headers.getSetCookie().filter((cookie) => cookie.startsWith("frigate_token=")); + if (cookies.length !== 1) throw new Error("Missing or ambiguous session cookie"); + const token = cookies[0]?.split(";", 1)[0]?.slice("frigate_token=".length); + if (!token || token.length > 4096 || !/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(token)) { + throw new Error("Invalid session cookie"); + } + const headers = { Authorization: `Bearer ${token}` }; + z.object({ username: z.literal(username), role: z.literal("admin"), allowed_cameras: z.array(z.string()) }).parse( + await context.requestAsync("/api/profile", { headers, signal: context.signal }), + ); + return { headers, redactValues: [token] }; + } catch { + throw new Error("Frigate authentication failed"); + } +} diff --git a/packages/integrations/src/stats/stats-integration.ts b/packages/integrations/src/stats/stats-integration.ts index c6405f5df..057adfb3c 100644 --- a/packages/integrations/src/stats/stats-integration.ts +++ b/packages/integrations/src/stats/stats-integration.ts @@ -29,9 +29,11 @@ export class StatsIntegration extends Integration { const signal = AbortSignal.timeout(15_000); return await this.provider.getHttpAuthenticationAsync({ signal, + url: new URL(this.integration.url), secret: (kind) => this.getSecretValue(kind), hasSecret: (kind) => this.hasSecretValue(kind), requestAsync: this.createRequestAsync(signal, undefined, undefined, true), + requestResponseAsync: this.createResponseRequestAsync(signal), }); } if (!this.provider.getHttpAuthentication) return await super.getHttpAuthenticationAsync(); @@ -49,12 +51,37 @@ export class StatsIntegration extends Integration { } return await this.provider.fetchAsync({ signal, + url: new URL(this.integration.url), secret: (kind) => this.getSecretValue(kind), hasSecret: (kind) => this.hasSecretValue(kind), requestAsync: this.createRequestAsync(signal, testing, authentication), + requestResponseAsync: this.createResponseRequestAsync(signal, testing), }); } + private createResponseRequestAsync( + signal: AbortSignal, + testing?: IntegrationTestingInput, + ): StatsFetchContext["requestResponseAsync"] { + return async (path, init) => { + try { + const fetchAsync = testing?.fetchAsync ?? fetchWithTrustedCertificatesAsync; + const response = await fetchAsync(this.url(path), { ...init, signal, redirect: "error" }); + // Login bodies can be empty or contain sensitive error details. Do not read them. + await response.body?.cancel(); + let headerBytes = 0; + for (const [name, value] of response.headers) { + headerBytes += Buffer.byteLength(name) + Buffer.byteLength(value); + if (headerBytes > 16_384) throw new Error("Response headers are too large"); + } + return { status: response.status, headers: response.headers }; + } catch { + // Fetch errors may retain the password body or session cookies in their cause. + throw new Error("Stats authentication request failed"); + } + }; + } + private createRequestAsync( signal: AbortSignal, testing?: IntegrationTestingInput, diff --git a/packages/integrations/src/stats/types.ts b/packages/integrations/src/stats/types.ts index 60f045ef4..c72606034 100644 --- a/packages/integrations/src/stats/types.ts +++ b/packages/integrations/src/stats/types.ts @@ -1,4 +1,4 @@ -import type { RequestInit } from "undici"; +import type { Headers, RequestInit } from "undici"; import type { IntegrationSecretKind } from "@homarr/definitions"; import type { IntegrationHttpAuthentication } from "../http-auth"; @@ -26,8 +26,12 @@ export interface StatsAuthenticationContext { } export interface StatsFetchContext extends StatsAuthenticationContext { + /** Base integration URL. Credential exchanges must verify its protocol before reading or sending secrets. */ + url: URL; /** Relative service path. The shared client applies trusted certificates and a deadline. */ requestAsync: (path: `/${string}`, init?: RequestInit) => Promise; + /** Private response metadata for cookie-based exchanges; the body is discarded, never parsed or exposed. */ + requestResponseAsync: (path: `/${string}`, init?: RequestInit) => Promise<{ status: number; headers: Headers }>; signal: AbortSignal; }