diff --git a/.env.example b/.env.example
index 318b171e1..22dca3adb 100644
--- a/.env.example
+++ b/.env.example
@@ -59,4 +59,11 @@ TURBO_TELEMETRY_DISABLED=1
# ENABLE_KUBERNETES=true
# Enable mock integration
-UNSAFE_ENABLE_MOCK_INTEGRATION=true
\ No newline at end of file
+UNSAFE_ENABLE_MOCK_INTEGRATION=true
+
+# Enable demo mode (seeds a demo user "demo"/"demo", a sample board and a mock integration)
+# DEMO_MODE=true
+
+# Whether demo mode is read-only. Defaults to true (the public demo blocks all mutations).
+# Set to false for writable preview deployments so the demo user can modify the dashboard.
+# DEMO_READ_ONLY=false
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index fbcd9d5c1..42f647d87 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -33,6 +33,9 @@ body:
options:
# The below comment is used to insert a new version with on-release.yml
#NEXT_VERSION#
+ - 1.70.0
+ - 1.69.2
+ - 1.69.0
- 1.68.0
- 1.67.0
- 1.66.1
diff --git a/.github/actions/prebuilt-debian/action.yaml b/.github/actions/prebuilt-debian/action.yaml
index af1b7fdad..6e8b04846 100644
--- a/.github/actions/prebuilt-debian/action.yaml
+++ b/.github/actions/prebuilt-debian/action.yaml
@@ -11,7 +11,7 @@ outputs:
runs:
using: "composite"
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Docker Buildx
diff --git a/.github/workflows/automatic-approval.yml b/.github/workflows/automatic-approval.yml
index 6647bad82..9e246f838 100644
--- a/.github/workflows/automatic-approval.yml
+++ b/.github/workflows/automatic-approval.yml
@@ -17,7 +17,7 @@ jobs:
if: github.actor_id == 158783068 || github.actor_id == 190541745 || github.actor_id == 210161987 || github.actor_id == 175486441
steps:
- name: Checkout code
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
- name: Obtain token
id: obtainToken
uses: actions/create-github-app-token@v3
diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml
index 8a01559a9..48d017cde 100644
--- a/.github/workflows/code-quality.yml
+++ b/.github/workflows/code-quality.yml
@@ -25,13 +25,13 @@ env:
jobs:
lint-format-autofix:
- if: github.event_name == 'pull_request'
+ if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.id == github.repository_id
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
with:
fetch-depth: 0
ref: ${{ github.head_ref }}
@@ -60,7 +60,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
with:
fetch-depth: 0
@@ -78,7 +78,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
with:
fetch-depth: 0
@@ -92,7 +92,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
with:
fetch-depth: 0
@@ -106,7 +106,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Setup
uses: ./tooling/github/setup
@@ -124,7 +124,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Setup
uses: ./tooling/github/setup
- name: Set up Docker Buildx
@@ -153,7 +153,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Setup
diff --git a/.github/workflows/crowdin-schedule-download.yml b/.github/workflows/crowdin-schedule-download.yml
index a5dec8869..22cb2b943 100644
--- a/.github/workflows/crowdin-schedule-download.yml
+++ b/.github/workflows/crowdin-schedule-download.yml
@@ -15,7 +15,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
- name: Obtain token
id: obtainToken
diff --git a/.github/workflows/crowdin-upload.yml b/.github/workflows/crowdin-upload.yml
index 4f8d48de6..25e9b44c3 100644
--- a/.github/workflows/crowdin-upload.yml
+++ b/.github/workflows/crowdin-upload.yml
@@ -18,7 +18,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
- name: Upload Crowdin translations
uses: crowdin/github-action@v2
diff --git a/.github/workflows/deployment-beta-release.yml b/.github/workflows/deployment-beta-release.yml
index fd79d1502..ba3bde3a2 100644
--- a/.github/workflows/deployment-beta-release.yml
+++ b/.github/workflows/deployment-beta-release.yml
@@ -26,7 +26,7 @@ jobs:
with:
args: "Beta release has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
- name: Checkout repository
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Get Next Version
diff --git a/.github/workflows/deployment-docker-image.yml b/.github/workflows/deployment-docker-image.yml
index 359ca09d7..3ab3a758e 100644
--- a/.github/workflows/deployment-docker-image.yml
+++ b/.github/workflows/deployment-docker-image.yml
@@ -57,7 +57,7 @@ jobs:
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # required to commit package.json & changelog changes, merge them to dev / next and publish the release
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
if: env.SKIP_RELEASE == 'false'
with:
persist-credentials: false
@@ -66,7 +66,7 @@ jobs:
- uses: actions/setup-node@v6
if: env.SKIP_RELEASE == 'false'
with:
- node-version: 24.17.0
+ node-version: 24.18.0
package-manager-cache: false
- run: npm i -g pnpm
if: env.SKIP_RELEASE == 'false'
@@ -119,7 +119,7 @@ jobs:
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
with:
ref: ${{ needs.release.outputs.git_ref }}
@@ -158,7 +158,7 @@ jobs:
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
with:
ref: ${{ needs.release.outputs.git_ref }}
diff --git a/.github/workflows/deployment-docs.yml b/.github/workflows/deployment-docs.yml
index 7e4aa4387..eeb53d101 100644
--- a/.github/workflows/deployment-docs.yml
+++ b/.github/workflows/deployment-docs.yml
@@ -30,7 +30,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Setup
uses: ./tooling/github/setup
- name: Build docs
diff --git a/.github/workflows/deployment-weekly-release.yml b/.github/workflows/deployment-weekly-release.yml
index 7c5c8d21d..cafa7b30d 100644
--- a/.github/workflows/deployment-weekly-release.yml
+++ b/.github/workflows/deployment-weekly-release.yml
@@ -29,7 +29,7 @@ jobs:
with:
args: "Automatic release has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
- name: Checkout repository
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Get Next Version
diff --git a/.github/workflows/docs-algolia-recrawl.yml b/.github/workflows/docs-algolia-recrawl.yml
index 63b335985..1d8bdd42d 100644
--- a/.github/workflows/docs-algolia-recrawl.yml
+++ b/.github/workflows/docs-algolia-recrawl.yml
@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Setup
uses: ./tooling/github/setup
diff --git a/.github/workflows/docs-update-contributors.yml b/.github/workflows/docs-update-contributors.yml
index 8e8aeac10..bbfbc10cb 100644
--- a/.github/workflows/docs-update-contributors.yml
+++ b/.github/workflows/docs-update-contributors.yml
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
with:
ref: dev
diff --git a/.github/workflows/docs-update-openapi.yml b/.github/workflows/docs-update-openapi.yml
index e8a1192a5..a2f1f7c30 100644
--- a/.github/workflows/docs-update-openapi.yml
+++ b/.github/workflows/docs-update-openapi.yml
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
with:
ref: dev
diff --git a/.github/workflows/on-pr-prebuilt-debian-validate.yml b/.github/workflows/on-pr-prebuilt-debian-validate.yml
index 025f513e3..a67dd1d62 100644
--- a/.github/workflows/on-pr-prebuilt-debian-validate.yml
+++ b/.github/workflows/on-pr-prebuilt-debian-validate.yml
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Validate prebuilt dependencies for amd64
id: validate-amd64
uses: ./.github/actions/prebuilt-debian
@@ -30,7 +30,7 @@ jobs:
runs-on: ubuntu-24.04-arm
timeout-minutes: 5
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Validate prebuilt dependencies for arm64
id: validate-arm64
uses: ./.github/actions/prebuilt-debian
diff --git a/.github/workflows/on-pr-renovate-validate.yml b/.github/workflows/on-pr-renovate-validate.yml
index cd5b27c49..4a77b1175 100644
--- a/.github/workflows/on-pr-renovate-validate.yml
+++ b/.github/workflows/on-pr-renovate-validate.yml
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- run: |
npx --yes --package renovate -- \
renovate-config-validator --strict .github/renovate.json5
diff --git a/.github/workflows/on-release.yml b/.github/workflows/on-release.yml
index 514f225b4..644f4a74d 100644
--- a/.github/workflows/on-release.yml
+++ b/.github/workflows/on-release.yml
@@ -52,7 +52,7 @@ jobs:
permission-contents: write # required to commit to branch
permission-pull-requests: write # required to create pr & enable automerge
- name: Checkout code
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
with:
token: ${{ steps.obtainToken.outputs.token }}
ref: dev
diff --git a/.github/workflows/update-contributors.yml b/.github/workflows/update-contributors.yml
index 925e490c7..bb4364596 100644
--- a/.github/workflows/update-contributors.yml
+++ b/.github/workflows/update-contributors.yml
@@ -29,7 +29,7 @@ jobs:
permission-pull-requests: write # required to create pr & enable automerge
- name: Checkout repository
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
diff --git a/.github/workflows/update-integration-list.yml b/.github/workflows/update-integration-list.yml
index 05bb05d27..d6fdcab73 100644
--- a/.github/workflows/update-integration-list.yml
+++ b/.github/workflows/update-integration-list.yml
@@ -28,7 +28,7 @@ jobs:
permission-contents: write # required to commit to branch
permission-pull-requests: write # required to create pr & enable automerge
- name: Checkout code
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
- name: Setup
diff --git a/.gitignore b/.gitignore
index 1bb62e979..c22262f6d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
# dependencies
node_modules
+.pnpm-store/
.pnp
.pnp.js
.idea/
@@ -70,7 +71,10 @@ apps/nextjs/public/images/background.png
# personal agent config
.opencode
+.agents/
+skills-lock.json
# docusaurus
apps/docs/.docusaurus
apps/docs/build
+.pnpm-store
diff --git a/.mise.toml b/.mise.toml
index 0b18e941c..d6bd41fc8 100644
--- a/.mise.toml
+++ b/.mise.toml
@@ -1,2 +1,2 @@
[tools]
-node = "24.17.0"
+node = "24.18.0"
diff --git a/.nvmrc b/.nvmrc
index 1dd37d537..ca5c35005 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-24.17.0
+24.18.0
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index f0cdbe63c..8f1b6afce 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -1,7 +1,7 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
- "esbenp.prettier-vscode",
+ "oxc.oxc-vscode",
"yoavbls.pretty-ts-errors",
"million.million-lint",
"lokalise.i18n-ally"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a37858942..4a6d38619 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,57 @@
+## [1.70.0](https://github.com/homarr-labs/homarr/compare/v1.69.2...v1.70.0) (2026-07-10)
+
+## [1.69.2](https://github.com/homarr-labs/homarr/compare/v1.69.1...v1.69.2) (2026-07-06)
+
+### Bug Fixes
+
+* **layout:** raise AppShellHeader z-index above Indicator dots ([687edb9](https://github.com/homarr-labs/homarr/commit/687edb9226307cb83986097abb24fe7f672e1778)), closes [#6194](https://github.com/homarr-labs/homarr/issues/6194)
+
+## [1.69.1](https://github.com/homarr-labs/homarr/compare/v1.69.0...v1.69.1) (2026-07-04)
+
+### Bug Fixes
+
+* unblock query cache mutations in demo mode and add cache buster ([#6176](https://github.com/homarr-labs/homarr/issues/6176)) ([a382c75](https://github.com/homarr-labs/homarr/commit/a382c75968e73bafff55b5890b1e95f0779b50d3))
+
+## [1.69.0](https://github.com/homarr-labs/homarr/compare/v1.68.1...v1.69.0) (2026-07-03)
+
+### Features
+
+* add per-widget secret storage for release provider authentication ([77884a3](https://github.com/homarr-labs/homarr/commit/77884a31794e12a6f7bc59d96c8f61aa429ca68d))
+* **config:** enable pnpm global virtual store for worktree support ([#6098](https://github.com/homarr-labs/homarr/issues/6098)) ([3864727](https://github.com/homarr-labs/homarr/commit/3864727502a64052c4e3838468094b30bf04a133))
+* **docker-widget:** add per-column visibility toggle ([#6093](https://github.com/homarr-labs/homarr/issues/6093)) ([0765262](https://github.com/homarr-labs/homarr/commit/076526293d13379004461633d7ba4c2e83bd5ffe))
+* **docs:** addd in documentation for archiveteam warrior ([f66469d](https://github.com/homarr-labs/homarr/commit/f66469d79797a2da214588f4e8253b03633c59a5))
+* simplify caching — TanStack Query as single source of truth ([#6075](https://github.com/homarr-labs/homarr/issues/6075)) ([f19033e](https://github.com/homarr-labs/homarr/commit/f19033e0d53d248aed678737f3d04d5d8c7815b5))
+* **widgets:** right-click context menu with auto-generated toggle actions ([#6126](https://github.com/homarr-labs/homarr/issues/6126)) ([7e58858](https://github.com/homarr-labs/homarr/commit/7e58858353ae866efd657270d4bb16d2c7d3e551))
+* **widgets:** show calendar events on hover with pin on click ([#2637](https://github.com/homarr-labs/homarr/issues/2637)) ([#6081](https://github.com/homarr-labs/homarr/issues/6081)) ([db3d588](https://github.com/homarr-labs/homarr/commit/db3d58821e4de8d46b110125d0fa36919765248f))
+
+### Bug Fixes
+
+* add id to repository schema and handle missing ids gracefully ([8747d5f](https://github.com/homarr-labs/homarr/commit/8747d5f93ee9a295aad3c7035e8c6c7080bf5ee6))
+* address PR review - atomic upsert, MCP metadata, stale tokens, save UX ([757d598](https://github.com/homarr-labs/homarr/commit/757d59854e5ec82d91c053b8bc3d7eaf2e70efc4))
+* address PR review findings for widget-only integration refactor ([c892731](https://github.com/homarr-labs/homarr/commit/c892731479b47fab6218bd1f52fa6eccbdaa371e))
+* address review - paginate GH releases, GitLab tag_name, docs, test ([e687bc8](https://github.com/homarr-labs/homarr/commit/e687bc8c9277797acaa164a09c74b3808a2cb346))
+* apply version filter to LinuxServer.io provider ([34d976e](https://github.com/homarr-labs/homarr/commit/34d976eeea09e396334ce8fce5c492c07e7dd32b))
+* catch individual repo fetch failures to prevent batch crashes ([d893515](https://github.com/homarr-labs/homarr/commit/d893515b932608ea08c9c6e90b83f340e40f6f8e))
+* compile versionRegex once and add fetch timeouts ([64c7512](https://github.com/homarr-labs/homarr/commit/64c75122833981a49e9c8bf1789d501faff6d62d))
+* **deps:** update dependency postcss to ^8.5.16 ([#6169](https://github.com/homarr-labs/homarr/issues/6169)) ([9252fdc](https://github.com/homarr-labs/homarr/commit/9252fdc13971bd982224a31d7be577112f99ec45))
+* dns-hole widgets showing stale disconnected state ([#6096](https://github.com/homarr-labs/homarr/issues/6096)) ([2443f1c](https://github.com/homarr-labs/homarr/commit/2443f1cd3ff6765a91783d60d42aa03d9ba63fcd))
+* gracefully degrade when widget_secret table is missing or access denied ([30107e4](https://github.com/homarr-labs/homarr/commit/30107e4e76a454ff7402de9a8bc2e0dd6f422aa8))
+* handle missing widgetSecrets table and add default releases repo ([af35a50](https://github.com/homarr-labs/homarr/commit/af35a50fa05fee2f0118e98727eb108b0200f762))
+* **immich:** v3 API compatibility — use searchAssets for album assets, drop deviceAssetId ([#5977](https://github.com/homarr-labs/homarr/issues/5977)) ([a5ec880](https://github.com/homarr-labs/homarr/commit/a5ec880eec276e0814d2ab3ed7f9bc0b804d0465))
+* quick wins — calendar, about, glances, admin UX, releases, iframe, notes ([#6116](https://github.com/homarr-labs/homarr/issues/6116)) ([f9da71a](https://github.com/homarr-labs/homarr/commit/f9da71a19bdfa589aafe26b164d0e0782208ac0a))
+* restore ArchiveTeam Warrior as a proper integration ([8a5ad58](https://github.com/homarr-labs/homarr/commit/8a5ad5861ba4c2d0978f1fa3f6115a1c5974f8a2))
+* **timetable:** restore station search by removing disabled query guard ([d670b69](https://github.com/homarr-labs/homarr/commit/d670b6939672932c1a3d17ab9913901491cb46e3))
+* update CHANGES.md for ArchiveTeam Warrior restoration, remove typo translation key ([62baaf7](https://github.com/homarr-labs/homarr/commit/62baaf78130c4b74e899a948cee17097d8c02593))
+* verbose error messages in all release providers ([2155f13](https://github.com/homarr-labs/homarr/commit/2155f13f245f601b99458e227b6e058fbfa44cd9))
+* **widgets:** make downloads widget table background transparent ([#6025](https://github.com/homarr-labs/homarr/issues/6025)) ([73bed51](https://github.com/homarr-labs/homarr/commit/73bed51df895250f415efd7a87e721e403b3fd99))
+
+## [1.68.1](https://github.com/homarr-labs/homarr/compare/v1.68.0...v1.68.1) (2026-06-30)
+
+### Bug Fixes
+
+* make source maps opt-in to restore v1.67.0 memory baseline ([57ef0af](https://github.com/homarr-labs/homarr/commit/57ef0af4a252ea39727caeba7e13587dabc6254e)), closes [#6123](https://github.com/homarr-labs/homarr/issues/6123)
+* revert production source maps to restore v1.67.0 memory baseline ([60a69e3](https://github.com/homarr-labs/homarr/commit/60a69e31efc7168b8f4309daf75954d7730d883b)), closes [#6123](https://github.com/homarr-labs/homarr/issues/6123)
+
## [1.68.0](https://github.com/homarr-labs/homarr/compare/v1.67.0...v1.68.0) (2026-06-26)
### Features
diff --git a/CHANGES.md b/CHANGES.md
new file mode 100644
index 000000000..c8a801420
--- /dev/null
+++ b/CHANGES.md
@@ -0,0 +1,96 @@
+# Changes
+
+## Goal
+
+Remove widget-only services from Homarr's global Integration system.
+
+The affected services are release providers and Search.ch. They are only used by specific widgets, so keeping them as global integrations added unnecessary setup, UI, database rows, docs pages, and integration package code.
+
+ArchiveTeam Warrior remains a proper integration but is no longer auto-seeded on first install.
+
+## Why
+
+These services do not need shared integration state.
+
+- Release providers are selected per Releases widget repository.
+- Search.ch is only used by the Timetable widget.
+
+Moving them into widget options makes configuration simpler and removes dead-weight integration code from `packages/integrations`.
+
+## What Changed
+
+### Release Providers
+
+- Added release provider definitions in `packages/definitions/src/release-provider.ts`.
+- Removed release providers from `IntegrationKind`.
+- Moved release-fetching code out of `packages/integrations` and into `packages/request-handler/src/release-providers.ts`.
+- Updated the Releases widget to store provider configuration directly in widget options:
+ - `provider`
+ - `identifier`
+ - `versionFilter`
+ - `providerUrl`
+- Updated the Releases widget editor to use a provider select instead of an integration select.
+- Updated Docker import mapping so `ghcr.io` maps to `gitHubContainerRegistry`.
+- GitHub Container Registry now uses the public OCI registry API and accepts full image references such as `ghcr.io/homarr-labs/homarr:latest`.
+
+### Timetable / Search.ch
+
+- Removed Search.ch as an integration.
+- Added a Timetable widget `baseUrl` option, defaulting to `https://search.ch`.
+- Moved Search.ch request/parsing logic into `packages/request-handler/src/timetable.ts`.
+- Updated the Timetable API to receive `baseUrl` directly from widget options.
+
+### ArchiveTeam Warrior
+
+- Kept as a proper integration (not removed).
+- Removed `defaultUrl` from the integration definition to prevent auto-seeding on first install.
+
+### Integration Cleanup
+
+- Removed creators, exports, docs references, and implementation files for the deleted integrations.
+- Removed unused dependencies from `packages/integrations/package.json`.
+
+### Database Migration
+
+- Added `packages/db/migrations/custom/0003_remove_widget_only_integrations.ts`.
+- The migration moves old integration-linked widget config into widget options, then deletes the old widget-only integration rows.
+- Updated the earlier release widget migration to use the new `provider` field instead of `providerIntegrationId`.
+
+### Docs
+
+- Updated Releases widget docs to describe providers instead of integrations.
+- Updated Timetable docs to remove Search.ch integration setup.
+- Removed obsolete integration docs pages for the deleted integration kinds.
+- Updated docs sitemap and integration docs mappings.
+
+### Translations
+
+- Added English labels for the new widget options:
+ - `widget.timetable.option.baseUrl.label`
+ - `widget.releases.option.repositories.providerUrl.label`
+
+## How It Works Now
+
+Release provider configuration lives on each Releases widget repository entry. The request handler resolves provider metadata and fetches releases directly from the provider-specific endpoint. GHCR uses anonymous OCI registry access for public packages.
+
+Timetable passes its widget URL option directly to its API router and request handler. No global integration lookup is needed.
+
+ArchiveTeam Warrior continues to use the standard integration system, but users must manually add the integration (it is not auto-created on install).
+
+## Deliberate Non-Goals
+
+- No provider secret/token support was added.
+- No new secret storage model was introduced.
+- No compatibility layer was kept in `packages/integrations`.
+
+Widget options are client-visible, so storing provider credentials there would be wrong. If authenticated provider requests are needed later, they need a separate secret-storage design.
+
+## Verification So Far
+
+- Focused typechecks passed for affected packages.
+- Full `pnpm turbo typecheck` passed.
+- Full `pnpm lint` passed with existing warnings.
+- Focused release provider normalization tests passed.
+- Widget translation test passed.
+- Live GHCR smoke checks passed for `ghcr.io/muchobien/pocketbase:latest` and `ghcr.io/homarr-labs/homarr:latest`.
+- `pnpm test packages/integrations/test/pi-hole.spec.ts` passed earlier, but failed on rebased reruns because Pi-hole reported `domainsBeingBlocked` as `-2`/`0` instead of `> 1`.
diff --git a/CODEOWNERS b/CODEOWNERS
index b3e2214ac..7082b9bac 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -1,12 +1,220 @@
# ── CODEOWNERS ───────────────────────────────────────
* @homarr-labs/maintainers
-# Specific features / integrations
-# Glueten
-apps/docs/docs/integrations/glueten @Lok3sh-kumar
+# Specific features / integrations
+# These owners come from the PR that introduced each integration implementation.
+# Specific rules override the global fallback because CODEOWNERS uses the last
+# matching pattern; do not include @homarr-labs/maintainers here unless they
+# should also be requested for that integration.
+
+# AdGuard Home - introduced in https://github.com/homarr-labs/homarr/pull/929 by @hillaliy
+apps/docs/docs/integrations/adguard-home @hillaliy
+packages/integrations/src/adguard-home @hillaliy
+
+# Anchor - introduced in https://github.com/homarr-labs/homarr/pull/5040 by @ohmzi
+apps/docs/docs/integrations/anchor @ohmzi
+packages/integrations/src/anchor @ohmzi
+
+# ArchiveTeam Warrior - introduced in https://github.com/homarr-labs/homarr/pull/5784 by @darbyjack
+apps/docs/docs/integrations/archive-team-warrior @darbyjack
+packages/integrations/src/archive-team-warrior @darbyjack
+
+# Bazarr - introduced in https://github.com/homarr-labs/homarr/pull/6156 by @Ayush7614
+apps/docs/docs/integrations/bazarr @Ayush7614
+apps/docs/docs/widgets/bazarr @Ayush7614
+packages/api/src/router/widgets/bazarr.ts @Ayush7614
+packages/integrations/src/bazarr @Ayush7614
+packages/request-handler/src/bazarr.ts @Ayush7614
+packages/widgets/src/bazarr @Ayush7614
+
+# Audiobookshelf - introduced in https://github.com/homarr-labs/homarr/pull/5910 by @ajnart
+apps/docs/docs/integrations/audiobookshelf @ajnart
+packages/integrations/src/audiobookshelf @ajnart
+
+# Beszel - introduced in https://github.com/homarr-labs/homarr/pull/5900 by @ajnart
+apps/docs/docs/integrations/beszel @ajnart
+packages/integrations/src/beszel @ajnart
+
+# Codeberg - introduced in https://github.com/homarr-labs/homarr/pull/3253 by @Aandree5
+apps/docs/docs/integrations/codeberg @Aandree5
+packages/integrations/src/codeberg @Aandree5
+
+# Coolify - introduced in https://github.com/homarr-labs/homarr/pull/4789 by @Rhiz3K
+apps/docs/docs/integrations/coolify @Rhiz3K
+packages/integrations/src/coolify @Rhiz3K
+
+# Dash. - introduced in https://github.com/homarr-labs/homarr/pull/1541 by @manuel-rw
+apps/docs/docs/integrations/dash-dot @manuel-rw
+packages/integrations/src/dashdot @manuel-rw
+
+# Docker Hub - introduced in https://github.com/homarr-labs/homarr/pull/3253 by @Aandree5
+apps/docs/docs/integrations/docker-hub @Aandree5
+packages/integrations/src/docker-hub @Aandree5
+
+# Emby - introduced in https://github.com/homarr-labs/homarr/pull/2466 by @Meierschlumpf
+apps/docs/docs/integrations/emby @Meierschlumpf
+packages/integrations/src/emby @Meierschlumpf
+
+# GitHub - introduced in https://github.com/homarr-labs/homarr/pull/3253 by @Aandree5
+apps/docs/docs/integrations/github @Aandree5
+packages/integrations/src/github @Aandree5
+
+# GitHub Container Registry - introduced in https://github.com/homarr-labs/homarr/pull/3727 by @Aandree5
+apps/docs/docs/integrations/github-containerregistry @Aandree5
+packages/integrations/src/github-container-registry @Aandree5
+
+# GitLab - introduced in https://github.com/homarr-labs/homarr/pull/3253 by @Aandree5
+apps/docs/docs/integrations/gitlab @Aandree5
+packages/integrations/src/gitlab @Aandree5
+
+# Glances - introduced in https://github.com/homarr-labs/homarr/pull/5102 by @manuel-rw
+apps/docs/docs/integrations/glances @manuel-rw
+packages/integrations/src/glances @manuel-rw
+
+# Gluetun - introduced in https://github.com/homarr-labs/homarr/pull/5812 by @Lok3sh-kumar
+apps/docs/docs/integrations/gluetun @Lok3sh-kumar
packages/integrations/src/gluetun @Lok3sh-kumar
-# Exempt Renovate‑managed files (no owners)
+# Gotify - introduced in https://github.com/homarr-labs/homarr/pull/5847 by @Nerdy-Technician
+apps/docs/docs/integrations/gotify @Nerdy-Technician
+packages/integrations/src/gotify @Nerdy-Technician
+
+# Home Assistant - introduced in https://github.com/homarr-labs/homarr/pull/578 by @manuel-rw
+apps/docs/docs/integrations/home-assistant @manuel-rw
+packages/integrations/src/homeassistant @manuel-rw
+
+# ICal - introduced in https://github.com/homarr-labs/homarr/pull/3980 by @NicolasNewman
+apps/docs/docs/integrations/ical @NicolasNewman
+packages/integrations/src/ical @NicolasNewman
+
+# Immich - introduced in https://github.com/homarr-labs/homarr/pull/5165 by @manuel-rw
+apps/docs/docs/integrations/immich @manuel-rw
+packages/integrations/src/immich @manuel-rw
+
+# Jellyfin - introduced in https://github.com/homarr-labs/homarr/pull/672 by @manuel-rw
+apps/docs/docs/integrations/jellyfin @manuel-rw
+packages/integrations/src/jellyfin @manuel-rw
+
+# Jellyseerr - introduced in https://github.com/homarr-labs/homarr/pull/774 by @manuel-rw
+apps/docs/docs/integrations/jellyseerr @manuel-rw
+packages/integrations/src/jellyseerr @manuel-rw
+
+# LinuxServer.io - introduced in https://github.com/homarr-labs/homarr/pull/3607 by @Aandree5
+apps/docs/docs/integrations/linux-server-io @Aandree5
+packages/integrations/src/linuxserverio @Aandree5
+
+# Navidrome - introduced in https://github.com/homarr-labs/homarr/pull/5910 by @ajnart
+apps/docs/docs/integrations/navidrome @ajnart
+packages/integrations/src/navidrome @ajnart
+
+# Nextcloud - introduced in https://github.com/homarr-labs/homarr/pull/2501 by @manuel-rw
+apps/docs/docs/integrations/nextcloud @manuel-rw
+packages/integrations/src/nextcloud @manuel-rw
+
+# npm - introduced in https://github.com/homarr-labs/homarr/pull/3253 by @Aandree5
+apps/docs/docs/integrations/npm @Aandree5
+packages/integrations/src/npm @Aandree5
+
+# ntfy - introduced in https://github.com/homarr-labs/homarr/pull/2900 by @itzTheMeow
+apps/docs/docs/integrations/ntfy @ajnart @manuel-rw
+packages/integrations/src/ntfy @ajnart @manuel-rw
+
+# OpenMediaVault - introduced in https://github.com/homarr-labs/homarr/pull/1142 by @hillaliy
+apps/docs/docs/integrations/open-media-vault @hillaliy
+packages/integrations/src/openmediavault @hillaliy
+
+# OPNsense - introduced in https://github.com/homarr-labs/homarr/pull/3424 by @Oupsman
+apps/docs/docs/integrations/opnsense @Oupsman
+packages/integrations/src/opnsense @Oupsman
+
+# Overseerr - introduced in https://github.com/homarr-labs/homarr/pull/774 by @manuel-rw
+apps/docs/docs/integrations/overseerr @manuel-rw
+packages/integrations/src/overseerr @manuel-rw
+
+# Paperless-ngx - introduced in https://github.com/homarr-labs/homarr/pull/5910 by @ajnart
+apps/docs/docs/integrations/paperless-ngx @ajnart
+packages/integrations/src/paperless-ngx @ajnart
+
+# PatchMon - introduced in https://github.com/homarr-labs/homarr/pull/6119 by @mrjoshuap
+apps/docs/docs/integrations/patchmon @mrjoshuap
+packages/integrations/src/patchmon @mrjoshuap
+packages/widgets/src/patchmon @mrjoshuap
+packages/api/src/router/widgets/patchmon.ts @mrjoshuap
+packages/request-handler/src/patchmon.ts @mrjoshuap
+
+# PeaNUT - introduced in https://github.com/homarr-labs/homarr/pull/5862 by @afonsojramos
+apps/docs/docs/integrations/peanut @afonsojramos
+packages/integrations/src/peanut @afonsojramos
+
+# Pi-hole - introduced in https://github.com/homarr-labs/homarr/pull/521 by @Meierschlumpf
+apps/docs/docs/integrations/pi-hole @Meierschlumpf
+packages/integrations/src/pi-hole @Meierschlumpf
+
+# Plex - introduced in https://github.com/homarr-labs/homarr/pull/1342 by @hillaliy
+apps/docs/docs/integrations/plex @hillaliy
+packages/integrations/src/plex @hillaliy
+
+# Prowlarr - introduced in https://github.com/homarr-labs/homarr/pull/965 by @hillaliy
+apps/docs/docs/integrations/prowlarr @hillaliy
+packages/integrations/src/prowlarr @hillaliy
+
+# Proxmox - introduced in https://github.com/homarr-labs/homarr/pull/1969 by @Meierschlumpf
+apps/docs/docs/integrations/proxmox @Meierschlumpf
+packages/integrations/src/proxmox @Meierschlumpf
+
+# Quay - introduced in https://github.com/homarr-labs/homarr/pull/3607 by @Aandree5
+apps/docs/docs/integrations/quay @Aandree5
+packages/integrations/src/quay @Aandree5
+
+# search.ch - introduced in https://github.com/homarr-labs/homarr/pull/5180 by @Meierschlumpf
+apps/docs/docs/integrations/search-ch @Meierschlumpf
+packages/integrations/src/search-ch @Meierschlumpf
+
+# Speedtest Tracker - introduced in https://github.com/homarr-labs/homarr/pull/5363 by @kylegrieder
+apps/docs/docs/integrations/speedtest-tracker @kylegrieder
+packages/integrations/src/speedtest-tracker @kylegrieder
+
+# Synology - introduced in https://github.com/homarr-labs/homarr/pull/6121 by @mrjoshuap
+apps/docs/docs/integrations/synology @mrjoshuap
+packages/integrations/src/synology @mrjoshuap
+
+# Technitium DNS - introduced in https://github.com/homarr-labs/homarr/pull/5832 by @lopsch
+apps/docs/docs/integrations/technitium-dns @lopsch
+packages/integrations/src/technitium @lopsch
+
+# Tracearr - introduced in https://github.com/homarr-labs/homarr/pull/5060 by @StrandedTurtle
+apps/docs/docs/integrations/tracearr @StrandedTurtle
+packages/integrations/src/tracearr @StrandedTurtle
+
+# TrueNAS - introduced in https://github.com/homarr-labs/homarr/pull/3745 by @manuel-rw
+apps/docs/docs/integrations/truenas @manuel-rw @afonsojramos
+packages/integrations/src/truenas @manuel-rw @afonsojramos
+
+# Umami - introduced in https://github.com/homarr-labs/homarr/pull/5362 by @febrile42
+apps/docs/docs/integrations/umami @febrile42
+packages/integrations/src/umami @febrile42
+
+# UniFi Controller - introduced in https://github.com/homarr-labs/homarr/pull/2236 by @pitschi
+apps/docs/docs/integrations/unifi-controller @pitschi
+packages/integrations/src/unifi-controller @pitschi
+
+# Unraid - introduced in https://github.com/homarr-labs/homarr/pull/4439 by @manuel-rw
+apps/docs/docs/integrations/unraid @manuel-rw
+packages/integrations/src/unraid @manuel-rw
+
+# Uptime Kuma - introduced in https://github.com/homarr-labs/homarr/pull/5910 by @ajnart
+apps/docs/docs/integrations/uptime-kuma @ajnart
+packages/integrations/src/uptime-kuma @ajnart
+
+# Traefik - by @Nerdy-Technician
+apps/docs/docs/integrations/traefik @Nerdy-Technician
+apps/docs/docs/widgets/traefik @Nerdy-Technician
+packages/api/src/router/widgets/traefik.ts @Nerdy-Technician
+packages/integrations/src/traefik @Nerdy-Technician
+packages/request-handler/src/traefik.ts @Nerdy-Technician
+packages/widgets/src/traefik @Nerdy-Technician
+
+# Exempt Renovate-managed files (no owners)
package.json
**/package.json
package-lock.json
diff --git a/Dockerfile b/Dockerfile
index d50756e39..e7b5b679c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:24.17.0-alpine AS base
+FROM node:24.18.0-alpine AS base
FROM base AS builder
RUN apk add --no-cache libc6-compat
@@ -20,17 +20,6 @@ ARG DISABLE_REDIS_LOGS='true'
RUN corepack enable pnpm && pnpm build
-# The standalone output tracing omits server chunk source maps (.next/server/**/*.map), which is
-# where production Node stack frames point. Copy the .map files into the standalone tree (preserving
-# directory structure) so the existing standalone COPY ships them, keeping traces readable.
-# See https://github.com/homarr-labs/homarr/issues/5891
-RUN cd apps/nextjs/.next/server && \
- find . -name '*.map' | while IFS= read -r map; do \
- dest="../standalone/apps/nextjs/.next/server/$map"; \
- mkdir -p "$(dirname "$dest")"; \
- cp "$map" "$dest"; \
- done
-
FROM base AS runner
WORKDIR /app
@@ -53,6 +42,7 @@ RUN mkdir -p /var/cache/nginx && \
COPY --from=builder /app/apps/nextjs/next.config.ts .
COPY --from=builder /app/apps/nextjs/package.json .
+COPY --from=builder /app/pnpm-workspace.yaml ./pnpm-workspace.yaml
COPY --from=builder /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node /app/build/better_sqlite3.node
@@ -75,9 +65,6 @@ ENV DB_DRIVER='better-sqlite3'
ENV AUTH_PROVIDERS='credentials'
ENV REDIS_IS_EXTERNAL='false'
ENV NODE_ENV='production'
-# Tell Node to symbolicate stack traces using the shipped .map files so production logs
-# show original source locations instead of minified frames, see https://github.com/homarr-labs/homarr/issues/5891
-ENV NODE_OPTIONS='--enable-source-maps'
ENTRYPOINT [ "/app/entrypoint.sh" ]
CMD ["sh", "run.sh"]
diff --git a/apps/docs/docs/getting-started/after-the-installation.mdx b/apps/docs/docs/getting-started/after-the-installation.mdx
index 5f923cec8..7ac904bf4 100644
--- a/apps/docs/docs/getting-started/after-the-installation.mdx
+++ b/apps/docs/docs/getting-started/after-the-installation.mdx
@@ -28,6 +28,7 @@ Read our [glossary](/docs/getting-started/glossary) if you are new to Homarr.
Congratulations on installing Homarr. Now it's time to get yourself familiar with the concepts of Homarr and how to use it.
## Onboarding
+
The onboard will show the first time you start Homarr.
This process will help you with your first steps and adding some initial data, such as the administrator account.
@@ -45,50 +46,68 @@ You can either import a previous dashboard using the ZIP archive or start from s
You may refresh anytime and continue from a different device - the onboarding is saved immediately on the server for your convenience.
### Starting from scratch
-Depending on your auth provider configuration you will either first see the admin user creation form or the external group creation form.
-If credentials provider was selected, you'll be prompted to create an admin user for Homarr.
-This user cannot be deleted and has full control over Homarr and all of it's boards.
-You must choose an username and password (both can later be changed).
-We enforce strict password requirements, when you enter your password, to protect your instance if you accidentially
-expose it to the Internet.
-
+Depending on your auth provider configuration you will either first see the admin user creation form or the external group creation form.
+If the credentials provider was selected, you'll be prompted to create an admin user for Homarr.
+This user cannot be deleted and has full control over Homarr and all of its boards.
+You must choose a username and password (both can later be changed).
+We enforce strict password requirements when you enter your password to protect your instance if you accidentally
+expose it to the internet.
+
+
If you selected external providers (such as LDAP or OIDC), you'll be prompted to create an admin group.
-This group will be used to synchronize users from your provider to Homarr.
-When a user is logged in through the provider the groups of him that match groups with any in Homarr will be added to the user.
-The name of the group has to match exactly the name of the group in the provider.
+This group will be used to synchronize users from your provider to Homarr.
+When a user logs in through the provider, any of their groups that match groups in Homarr will be assigned to the user.
+The name of the group must match exactly the name of the group in the provider.
-
+
Next, you can configure some basic settings for Homarr. We recommend to leave all settings at default for most users.
Here you can also disable analytics. Please note that our analytics are completly anonymous and is fully compliant with GDPR and PECR.
-
+
### Setting up integrations
After configuring server settings, the onboarding guides you through setting up your integrations.
#### Docker service discovery
+
If Docker is available (socket mounted), Homarr automatically detects running containers and matches them to known integration types.
Matching integrations are pre-selected in the integration list, saving you from manually picking each one.
#### URL templating
+
You can define a base URL template to auto-fill integration URLs in bulk.
Two modes are supported:
+
- **Subdomain mode** - e.g. `{app}.example.com` where `{app}` is replaced with the integration name
- **Port mode** - e.g. `192.168.1.1:{port}` where `{port}` is replaced with the integration's default port
#### API key helper links
+
For integrations that require API keys (such as Sonarr, Radarr, SABnzbd, or Seerr), a helper link opens the relevant settings page in the target application so you can quickly copy your key.
#### Creating apps from Docker containers
+
You can optionally auto-create bookmark apps for all discovered Docker containers, making them immediately available on your board.
:::note
-Some niche and registry integrations (GitHub, npm, Docker Hub, GitLab, Codeberg, LinuxServer.io, GHCR, Quay, Search.ch) are hidden from the onboarding selection grid but remain available on the manage integrations page.
+Release providers and Search.ch are configured directly in their widgets instead of the integrations page.
:::
@@ -97,30 +116,41 @@ The onboarding can no longer be started again.
From here you can click on one of the provided links to continue your journey.
We recommend you to continue with the section [creating your first board on this page](#creating-your-first-board).
-
+
### Importing a ZIP from version before 1.0.0
+
This is recommended when you used Homarr before and want to re-import your data.
It is important that you first migrate correctly using [the list of breaking changes for 1.0](/blog/2024/09/23/version-1.0).
-
+
**How to export and import data**
-1. Navigate to your old Homarr, open your management pages, open the tools submenu and open ``Migrate to 1.0``.
+
+1. Navigate to your old Homarr, open your management pages, open the tools submenu and open `Migrate to 1.0`.
2. Check the contents that you want to export. We recommend you to export all data.
3. Click on the export data button
4. Save the ZIP file, that is being downloaded, on your device. If you selected "integrations" or "users", you also get an encryption / decryption key.
-Copy it and do not loose it!
-5. Go to your new >1.0.0 Homarr instance and start the onboarding. Choose ``Import from Homarr before 1.0``
+ Copy it and do not loose it!
+5. Go to your new >1.0.0 Homarr instance and start the onboarding. Choose `Import from Homarr before 1.0`
6. Drag and drop your previously downloaded ZIP archive into the file upload:
7. Next, choose the data that you want to import and how to import it. Since screen sizes were removed in 1.0, we create a new board for every screen size by default. We recommend you to only keep the best layout and adjust it later in 1.0.0 - but you can also import all of them and pick later.
8. If you previously exported integrations or users, you will be asked to decrypt the data safely using your obtained encryption token. Paste into the modal and confirm your action.
9. Next, you can [follow the same setup as when starting from scratch](#starting-from-scratch). After you completed the onboarding, all of your data will be migrated and available.
## Creating your first board
+
In Homarr, you organize your apps and widgets on boards.
Boards can be organized using the interactive drag and drop.
-Let's create your first board by navigating here: ``Manage`` > ``Boards`` > ``New board``:
+Let's create your first board by navigating here: `Manage` > `Boards` > `New board`:

A board must always have a name and can either be public or private.
@@ -133,12 +163,13 @@ Congratulations - you've just created your first board. You can see boards, that

## Add your first app to Homarr
+
To add links to your dashboard, you must first add them as an "app".
Navigate to this page to add an app:

-Next, click on the ``New app`` button. You'll need to provide a name and an icon to create an app.
+Next, click on the `New app` button. You'll need to provide a name and an icon to create an app.
The name must be at least one character long but you can provide any image URL for the icon.
Homarr ships with over 11K icons - so you can just search by entering a search text instead.
@@ -149,8 +180,9 @@ After you created the app, you'll see in on the apps page:

## Arrange and organize your board
+
Let's add your freshly created app, from the previous step, to the board.
-To do this, edit your previously created board by clicking on the big ``Open board`` button:
+To do this, edit your previously created board by clicking on the big `Open board` button:

@@ -165,23 +197,38 @@ Here you have the following options in the header:
When entering the edit mode, you'll get some additional buttons to work with:
-
+
- **Add item**: Click this button to add new elements to your page.
- **Exit edit mode**: Click this btuton to save your changes and exit edit mode.
To continue, click the option "New Item":
-
+
Next, you can choose what item you'd like to add.
We will discuss the other options in the following sections - for now choose "App" to continue.
-
+
As soon as you chose the app, the modal will close and you'll get the following item on your board:
-
+
This is expected, because you haven't told Homarr what app it should show in this item.
To configure this, click on the small 3 dots at the top of the item in the edit mode and choose "Edit item" in the menu.
@@ -228,17 +275,18 @@ Be sure to save your changes by exiting edit mode!

## Adding your first integration
+
Now it's time to integrate some basic data into your board.
To do this, we first need to create an integration.
An integration is a connection between Homarr and a third party application - usually something that you run and deploy yourself.
-Navigate to ``Manage`` > ``Integrations`` (for example via your user menu).
+Navigate to `Manage` > `Integrations` (for example via your user menu).
As an example, we are going to use the public demo instance of **Dash.** - a popular application to monitor CPU / hardware usage.
Choose the **Dash.** integration from the list of available integrations:

-Next, choose a name (or leave it at default), enter ``https://dash.mauz.dev/`` as the URL in the form and click on ``Test connection and create``.
+Next, choose a name (or leave it at default), enter `https://dash.mauz.dev/` as the URL in the form and click on `Test connection and create`.
Pressing the save button will perform a test request to the integration, to confirm that the networking setup is working as expected.
Sometimes invalid configurations, firewalls and other mistakes can lead to connectivity problems - [follow this guide here](/docs/management/integrations/#troubleshooting-connection) if you experience issues.
@@ -254,13 +302,14 @@ the target application or your router.
:::
## Embed integration data using widgets
+
Let's add your integration, that you have configured in the section above, as a widget to your board.
First, navigate back to your board that you want to add it to.
Next, enter edit mode again and add another item to your board:

-Instead of choosing an app, let's choose ``System Health Monitoring`` this time.
+Instead of choosing an app, let's choose `System Health Monitoring` this time.
Click on "Add to board":

@@ -290,10 +339,12 @@ As with every change, don't forget to save your changes using the exit edit mode

## Grouping your items
+
As the last step, let's organize your freshly created items using two different options.
You can choose either or both of them on your boards.
### Using dynamic sections
+
Dynamic sections are a simple, resizable visual group to group items.

@@ -303,6 +354,7 @@ After adding a dynamic section, you can simply drag and drop items into the sect

### Using categories
+
Categories are more advanced and complicated than dynamic sections.
To create a category, open the add item modal and choose "new category":
@@ -329,22 +381,27 @@ You cannot reduce their horizontal width.
After completing the onboarding, Homarr provides interactive guided tours to help you discover key features.
### Management tour
+
When you first visit the management area (`/manage`), Homarr walks you through the main pages: boards, apps, integrations, and users (admin only).
Each page highlights the list view and the create action so you know where to find and add things.
### Board tour
+
When you first open a board, a tour highlights the board header elements: search, edit mode, settings, board switcher, and user menu.
### Tour controls
+
- Press **Enter** (or click **Next**) to advance through tour steps
- Tours auto-start on first visit and do not repeat once completed
- Tours are skipped on mobile screens (below `sm` breakpoint)
- You can **reset tours** from the user settings page under **Manage > Users > [Your user] > General**
## Next steps
+
You're done with the basic setup 🔥. Now it's time to organize and customize even more.
We highly recommend you to experiment with different options, styles and configurations.
See these curated pages for your next steps:
+
- Create new apps using the [apps management](/docs/management/apps/)
- Customize your board or add more boards using the [boards management](/docs/management/boards/)
- Integrate software that you use using the [integrations management](/docs/management/integrations/)
diff --git a/apps/docs/docs/getting-started/installation/helm.md b/apps/docs/docs/getting-started/installation/helm.md
index 8a3903bf1..1f6aa6e9e 100644
--- a/apps/docs/docs/getting-started/installation/helm.md
+++ b/apps/docs/docs/getting-started/installation/helm.md
@@ -2,9 +2,9 @@
-
+

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