Compare commits

..
Author SHA1 Message Date
ajnart 1735aeb139 style: format merged dev files 2026-09-10 19:58:44 +02:00
ajnart 92a7a83e92 chore: retain evaluation artifact ignore 2026-09-10 19:56:05 +02:00
ajnart 0b8e2e0927 merge: sync v2 stack base with dev 2026-09-10 19:53:57 +02:00
Thomas 1fbe844598 fix(settings): move board access control out of the nested form (#6662) 2026-09-10 18:13:56 +02:00
Thomas 365ef7f02b Revert "feat: announce Homarr v2 beta" (#6813) 2026-09-10 16:36:54 +02:00
homarr-releases[bot] 2b1a03849d chore: update bug report template (#6798) 2026-09-06 22:31:26 +02:00
Releases Homarr 4c243bc374 chore(release): 1.77.0 [skip ci] 2026-09-06 19:31:37 +00:00
homarr-releases[bot] ecd02f73c0 chore(release): automatic release v1.77.0 2026-09-06 19:30:29 +00:00
Thomas cd23b36f3c fix(ci): use pinned pnpm for releases (#6796) 2026-09-06 21:18:30 +02:00
homarr-releases[bot] f7f29eade9 chore(release): automatic release v1.77.0 2026-09-06 18:47:01 +00:00
Thomas 32a51c1e60 feat: announce Homarr v2 beta (#6793)
* feat: announce Homarr v2 beta

* chore: refine v2 beta announcement

* feat: expand v2 beta guidance

* chore: refine v2 beta resources

* chore: simplify v2 beta modal

* chore: improve v2 beta feedback links

* fix: keep v2 modal above header

* chore: sharpen v2 beta message

* fix: clear header for v2 modal
2026-09-06 20:36:04 +02:00
homarr-crowdin[bot] da32acb4f0 chore(lang): updated translations from crowdin
* chore(lang): update translations from crowdin

* chore(lang): update translations from crowdin

* chore(lang): update translations from crowdin

---------

Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2026-09-06 01:52:04 +00:00
homarr-update-contributors[bot] 1da13d9bb3 chore: update contributors (#6767)
Co-authored-by: Homarr Update Contributors <210161987+homarr-update-contributors[bot]@users.noreply.github.com>
2026-09-04 15:58:09 +00:00
homarr-crowdin[bot] cb0fec088f chore(lang): updated translations from crowdin
* chore(lang): update translations from crowdin

* chore(lang): update translations from crowdin

---------

Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2026-09-03 02:05:21 +00:00
homarr-crowdin[bot] a2fe8dd461 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2026-09-01 02:36:36 +00:00
homarr-update-contributors[bot] b6dfcd69ba chore: update contributors (#6735)
Co-authored-by: Homarr Update Contributors <210161987+homarr-update-contributors[bot]@users.noreply.github.com>
2026-08-28 21:54:00 +00:00
homarr-crowdin[bot]andThomas a38b0ef71d chore(lang): updated translations from crowdin (#6732)
Co-authored-by: Thomas <thomas@ajnart.fr>
2026-08-28 14:07:42 +02:00
Thomas 2cf3027eeb Merge pull request #6720 from homarr-labs/docs/update-helm-docs
docs(helm): update chart installation page
2026-08-28 08:32:40 +02:00
homarr-releases[bot] fa18f6fc71 chore: update bug report template (#6724) 2026-08-28 08:32:03 +02:00
Releases Homarr 331925c197 chore(release): 1.76.2 [skip ci] 2026-08-27 20:09:22 +00:00
Thomas 0e51f745d3 Merge pull request #6723 from homarr-labs/dev
chore(release): automatic release v1.76.2
2026-08-27 22:08:05 +02:00
Thomas d6f720d0e6 Revert "feat(http): set custom User-Agent on outbound integration requests (#6606)" (#6722)
This reverts commit ddbf05524b.
2026-08-27 22:00:23 +02:00
Thomas 5324bf12d4 fix(startup): use stable internal service addresses (#6721) 2026-08-27 21:52:54 +02:00
github-actions[bot] 2922a02ecf docs(helm): update chart installation page 2026-08-27 17:09:27 +00:00
Thomas 87cdcf28e0 fix(mcp): return valid string for void tool results (#6703) 2026-08-27 11:10:37 +02:00
homarr-releases[bot] 441b181e79 chore: update bug report template (#6716)
Co-authored-by: Releases Homarr <175486441+homarr-releases[bot]@users.noreply.github.com>
2026-08-27 11:08:43 +02:00
Releases Homarr eba639b59c chore(release): 1.76.1 [skip ci] 2026-08-26 16:43:52 +00:00
Thomas Camlong da36283ad1 chore(release): retrigger release pipeline 2026-08-26 18:30:12 +02:00
Thomas Camlong c050368782 chore(release): trigger release pipeline 2026-08-26 18:16:26 +02:00
homarr-releases[bot] 82b50f0fd7 chore(release): automatic release v1.76.1 2026-08-26 15:50:55 +00:00
Thomas a0149d5894 Merge pull request #6673 from homarr-labs/investigate-6672-immich-401 2026-08-22 19:44:03 +02:00
ajnart 8579fcbfc2 test(http,immich): address CodeRabbit review
- guarantee server teardown via try/finally in both wire-level tests
- add tuple-array HeadersInit coverage to the helper suite
2026-08-22 17:21:38 +02:00
ajnart b6f3aba23e test(http,immich): prove User-Agent fix at the wire level
Add two regression tests that fail on v1.76.0 and pass with the fix:
- wire-level test: a Headers instance (the shape @immich/sdk hands over)
  must reach the remote server with all headers intact
- real-SDK e2e: getServerStatistics through fetchWithTrustedCertificatesAsync
  against a live local server must deliver x-api-key
2026-08-22 17:16:31 +02:00
ajnart 31e218295d fix(http): preserve SDK headers when injecting User-Agent
The v1.76.0 User-Agent injection object-spread `fetchOptions.headers`, which
silently drops all headers when the value is a Headers instance. The Immich
SDK passes a Headers object with the merged x-api-key header, so requests
lost their API key and returned 401 (issue #6672). Build a real Headers
instance instead, keeping case-insensitive user-agent handling.
2026-08-22 17:12:43 +02:00
Thomas aca3b5d861 fix: correct Unraid CPU, link context menus, and board resizing (#6510)
Fix Unraid logical CPU averaging, preserve native context menus for app and bookmark widgets in view mode, and prevent scrollbar-width GridStack resize oscillation.
2026-08-22 10:53:26 +02:00
homarr-crowdin[bot] a7a2f0223a chore(lang): updated translations from crowdin (#6596)
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2026-08-22 08:49:51 +02:00
homarr-releases[bot] 7773fdc10b chore: update bug report template (#6655) 2026-08-21 22:14:45 +02:00
Thomas 7019a1d0a4 Merge pull request #6656 from homarr-labs/docs/update-helm-docs 2026-08-21 22:14:26 +02:00
github-actions[bot] 2d535caeac docs(helm): update chart installation page 2026-08-21 17:41:09 +00:00
Releases Homarr 2013fbe80d chore(release): 1.76.0 [skip ci] 2026-08-21 17:20:50 +00:00
homarr-releases[bot] 219ef58926 chore(release): automatic release v1.76.0 2026-08-21 17:19:40 +00:00
homarr-update-contributors[bot] 97195bc463 chore: update contributors (#6649)
Co-authored-by: Homarr Update Contributors <210161987+homarr-update-contributors[bot]@users.noreply.github.com>
2026-08-21 12:53:40 +00:00
Danura30082 73a742154e docs: Proxmox certificate obtention instructions (#6613) 2026-08-20 18:51:58 +02:00
Afonso Jorge Ramos 993a267e0c fix(truenas): keep reporting pools without usable dataset space (#6635) 2026-08-20 15:37:10 +02:00
Thomas f1c49d1fbf fix(startup): only chown runtime paths for custom PUID/PGID (#6598) 2026-08-19 08:29:17 +02:00
Thomas 95c66c61eb fix(widgets): stop Health Monitoring system tab firing NOT_FOUND when Proxmox is mixed in (#6623) 2026-08-18 19:56:47 +02:00
Thomas ec2f8c6c2c fix(widgets): match OMV disk temperature via normalized partition names (#6615) (#6619)
OMV reports filesystem devices (sda1) without the /dev/ prefix, so the
partition-suffix normalization never applied and filesystem-to-SMART
matching returned no temperature. Strip partition suffixes from
unprefixed sd/vd, nvme and mmcblk names, and use the shared normalized
matcher in both the health monitoring and system disks widgets.
2026-08-18 19:39:27 +02:00
Thomas 05ba57c2fa chore: migrate cursor rules to portable agent skills + mantine MCP guidance
* chore: migrate cursor rules to portable agent skills and add mantine MCP guidance

Replace Cursor-only .cursor/rules/*.mdc with Agent Skills open-standard
SKILL.md files in .agents/skills/ (canonical), mirrored to .cursor/skills/
and .claude/skills/ so they work across Claude Code, Cursor, Codex, and
OpenCode. Add mantine MCP server guidance to AGENTS.md and un-ignore
.agents/skills/ in .gitignore.

* refactor: single-source agent skills at .agents/skills

Cursor, Codex, and OpenCode all read .agents/skills/ natively, so the
.cursor/skills/ mirror was redundant. Claude Code reads .claude/skills/,
which is now a symlink back to .agents/skills/ so the content lives once.
Deletes .cursor/skills/ and converts .claude/skills/ to a symlink.

* chore: ship mantine MCP server via project-scoped .mcp.json

Declares the keyless @mantine/mcp-server in .mcp.json (Claude Code
project-scope standard, committable) and documents MCP server setup and
the mantine entry in AGENTS.md.

* chore: address coderabbit security-review and markdown lint findings

Add explicit security-review guidance before exposing MCP tools and label
plain-text code fences to satisfy markdownlint MD040.
2026-08-16 16:27:51 +02:00
Thomas ddbf05524b feat(http): set custom User-Agent on outbound integration requests (#6606)
* feat(http): set custom User-Agent on outbound integration requests

Set a default 'User-Agent: Homarr/<version> (+https://homarr.dev)' header
on all outbound HTTP requests made via fetchWithTrustedCertificatesAsync,
so Homarr's integration traffic is distinguishable in reverse-proxy,
WAF, and security-monitoring logs instead of appearing as generic undici.

Closes #6604

* refactor(version): centralize Homarr version in @homarr/version package

Replace fragile deep-relative 'package.json' imports across the monorepo
with a single source of truth: the new @homarr/version leaf package reads
the version once from the root package.json and exports it as VERSION.

Consumers now import by package name incl. the new User-Agent header in
fetchWithTrustedCertificatesAsync (packages/core):
  - core, api (info router), analytics, request-handler (update checker)
This also removes all manually-maintained relative version imports.

* fix(http): normalize User-Agent injection with Headers

Object-spreading fetchOptions.headers breaks when callers pass a Headers
instance (spread yields no entries) or a tuple array (numeric keys), and is
case-sensitive. Build a Headers instance and set the default User-Agent only
when no case-insensitive User-Agent entry exists, via a shared helper in both
fetch paths.

Adds unit tests covering record, Headers, tuple-array, and casing variants.

* revert(version): undo version refactor and tests, keep single-file change

Keep the User-Agent change as a single edit to
packages/core/src/infrastructure/http/request.ts. Remove the @homarr/version
package, the consumer migrations, and the unit tests.
2026-08-16 16:09:15 +02:00
Thomas df438b4cd2 Merge pull request #6571 from homarr-labs/feat/fix-home-assistant-actions 2026-08-16 00:23:14 +02:00
Thomas a1f91eb385 Merge pull request #6567 from homarr-labs/feat/fix-truenas-free-space 2026-08-15 20:20:35 +02:00
Thomas 8d5ffb2dd9 feat(paseo): add worktree install script (#6599)
* feat(paseo): add worktree install script

* feat(paseo): inline worktree setup in paseo.json

* feat(paseo): add scripts and teardown to paseo.json

* feat(paseo): expand workspace scripts catalog

* feat(paseo): reset encryption key in worktree env
2026-08-15 12:29:52 +02:00
Thomas Camlong 56e6b23af6 test(e2e): remove flaky credentials onboarding test 2026-08-15 11:05:06 +02:00
Thomas Camlong 5488bf4d18 fix(truenas): use lowercase in operator for dataset query 2026-08-15 01:33:20 +02:00
homarr-crowdin[bot] 621ea07549 chore(lang): updated translations from crowdin (#6553) 2026-08-14 20:39:02 +02:00
homarr-update-contributors[bot] f64c01456b chore: update contributors (#6588) 2026-08-14 20:38:42 +02:00
homarr-releases[bot] 56198c3f02 chore: update bug report template (#6591) 2026-08-14 20:38:26 +02:00
Thomas ca1bb6f341 Merge pull request #6592 from homarr-labs/docs/update-helm-docs
docs(helm): update chart installation page
2026-08-14 20:38:10 +02:00
github-actions[bot] bf6b6663cd docs(helm): update chart installation page 2026-08-14 18:25:32 +00:00
Releases Homarr 5c2d86767f chore(release): 1.75.0 [skip ci] 2026-08-14 17:45:24 +00:00
homarr-releases[bot] 7c75642d4d chore(release): automatic release v1.75.0 2026-08-14 17:43:56 +00:00
ajnart e41d938a5f test: match custom widget cache version 2026-08-14 11:07:02 +02:00
ajnart afa54755fd fix custom widget review findings 2026-08-14 10:35:21 +02:00
ajnart 0482c040ec chore: reconcile v2 after stack rebase 2026-08-13 19:59:46 +02:00
Thomas Camlong 5dc4dbeaef style: format v2 workshop files 2026-08-13 19:51:44 +02:00
Thomas f3f2778f1f test: cover public custom widgets for guests (#6558) 2026-08-13 19:51:44 +02:00
Thomas c08b8979ee feat: rewrite image picker (#6556)
* feat: rewrite image picker

* style: auto-fix lint and format

* fix: address image picker review

* fix: preserve unicode icon searches

* test: stabilize custom widget login

* fix: suggest image searches only on create
2026-08-13 19:51:44 +02:00
Thomas Camlong 9a7a828618 ci: retry flaky container-backed unit tests in fast gate
Integration tests (nzbget, nextcloud, pi-hole, etc.) start Docker
containers and intermittently time out under shared-runner load, which
fails the fast gate and skips the container job that publishes the PR
preview image. Retry failing unit tests twice so transient container
startup failures do not block the image publish.
2026-08-13 19:51:44 +02:00
Thomas Camlong 352a8ed0d0 ci: make e2e tests resilient to CI resource contention
E2E specs spin up Docker containers in parallel and intermittently time
out waiting for UI elements under shared-runner load. Limit parallel
workers to 2 and retry flaky failures twice so the container job can
complete and publish the PR preview image.
2026-08-13 19:51:44 +02:00
ajnart 6d3f7e3e36 test: wait for Home Assistant API readiness 2026-08-13 19:51:44 +02:00
Thomas Camlong 7833cca86b feat(workshop): add social previews 2026-08-13 19:51:44 +02:00
Thomas Camlong 6edf78acd7 fix: persist Workshop PocketBase data 2026-08-13 19:51:44 +02:00
Thomas Camlong 7d43f6e5d9 fix: defer Spotlight without loading feedback 2026-08-13 19:51:44 +02:00
Thomas Camlong 1723a9e744 fix: polish settings and command menu 2026-08-13 19:51:44 +02:00
Thomas Camlong 2ba31fd37b fix(workshop): streamline import review 2026-08-13 19:51:44 +02:00
Thomas Camlong 327e5c15ae fix(workshop): stack widget install flow 2026-08-13 19:51:44 +02:00
Thomas Camlong 3efe64a062 fix(workshop): expose moderation 2026-08-13 19:51:44 +02:00
Thomas Camlong 40b726b8bc fix: simplify custom widget preview controls 2026-08-13 19:51:44 +02:00
Thomas Camlong d2d0d16e4d fix(workshop): use PocketBase name 2026-08-13 19:51:44 +02:00
Thomas Camlong 63ad879022 fix(workshop): use GitHub handles in Homarr 2026-08-13 19:51:44 +02:00
Thomas Camlong 2ae0c2ab43 fix(docs): exclude generated build from typecheck 2026-08-13 19:51:44 +02:00
Thomas Camlong 2410ba5f5c fix(workshop): simplify GitHub identities 2026-08-13 19:51:25 +02:00
Thomas Camlong 8f0039245d docs: update WUD OpenAPI schema 2026-08-13 19:51:25 +02:00
Thomas Camlong 0871e4cf0d test: validate Workshop source locale 2026-08-13 19:51:25 +02:00
Thomas Camlong db9f42520a fix: stabilize v2 CI 2026-08-13 19:51:25 +02:00
Thomas Camlong 3ebcedec26 feat: add Workshop staging deployment 2026-08-13 19:51:25 +02:00
Thomas Camlong c41a3d427e fix: allow Workshop runtime config writes 2026-08-13 19:51:25 +02:00
Thomas Camlong 71c39aa18e fix: restore Workshop runtime URLs 2026-08-13 19:51:25 +02:00
Thomas Camlong 000c51a131 test: harden integration icon checks 2026-08-13 19:51:25 +02:00
Thomas Camlong 2c44c7f2b8 test: fix custom option repair fixture 2026-08-13 19:51:25 +02:00
Thomas Camlong c2cf25b4fb fix: repair stale custom widget options 2026-08-13 19:51:25 +02:00
Thomas Camlong 03ebb8c2db test(e2e): serialize container specs 2026-08-13 19:51:25 +02:00
Thomas Camlong f0b153aa37 fix(settings): avoid nested forms 2026-08-13 19:51:25 +02:00
Thomas Camlong 965d130ce7 fix(clock): defer live time until mount 2026-08-13 19:51:25 +02:00
Thomas Camlong a87a16edb1 fix(e2e): stabilize initial board hydration 2026-08-13 19:51:25 +02:00
Thomas Camlong 1493252e28 fix(clock): suppress live timestamp hydration noise 2026-08-13 19:51:25 +02:00
Thomas Camlong 46ff20b0a0 test(v2): make board tour dismissal optional 2026-08-13 19:51:25 +02:00
Thomas Camlong 1bc84dd90e test(v2): stabilize production E2E flows 2026-08-13 19:51:24 +02:00
Thomas Camlong 14cbc03621 fix(ci): exclude e2e project from unit gate 2026-08-13 19:51:08 +02:00
Thomas Camlong 9ec35b0cda fix(ci): exclude e2e tests from unit gate 2026-08-13 19:51:08 +02:00
Thomas Camlong 2d6dd2f0d0 fix(ci): detect custom widget runtime chunks 2026-08-13 19:51:08 +02:00
Thomas Camlong 572bc71396 fix(ci): refresh v2 generated artifacts 2026-08-13 19:51:08 +02:00
Thomas Camlong d4b1739c46 fix(ci): align Workshop pnpm version 2026-08-13 19:50:46 +02:00
ajnart 76495948e2 ci(workshop): restore image publishing 2026-08-13 19:50:46 +02:00
Thomas a95052c787 fix(workshop): harden deployment and deep links (#6542) 2026-08-13 19:50:46 +02:00
Thomas 400127b45c fix: harden v2 custom widgets and Workshop (#6480) 2026-08-13 19:50:46 +02:00
Thomas f77c47b94b Delete review.md 2026-08-13 19:49:25 +02:00
Thomas 825636c4f6 fix(custom-widgets): address final v2 review feedback (#6451) 2026-08-13 19:49:25 +02:00
Thomas 3aa14e22ed fix(custom-widgets): address v2 merge review (#6446) 2026-08-13 19:49:25 +02:00
Thomas acdfcbb18e feat(custom-widgets): complete Custom JSX v2 (#6390) 2026-08-13 19:48:57 +02:00
Thomas 3a1245cbc5 fix(custom-widgets): fix v2 editor crash and definition-not-found race (#6377) 2026-08-13 19:47:16 +02:00
Thomas Camlong f0963405ca test(integrations): wait for Home Assistant restart 2026-08-13 19:47:16 +02:00
Thomas Camlong d078d761d9 ci: publish multi-platform preview images 2026-08-13 19:47:16 +02:00
Thomas Camlong 84fcf02729 test(integrations): allow recurring calendar setup 2026-08-13 19:47:06 +02:00
Thomas Camlong c308ae5c6e fix(ci): prevent autofix race 2026-08-13 19:46:51 +02:00
Thomas Camlong 93192cfedc fix(ci): autofix commits skip CI to prevent race cancellation 2026-08-13 19:46:45 +02:00
Thomas Camlong 0f9ae13560 fix(ci): remove coverage step, fix auto-fix to format all affected workspaces 2026-08-13 19:46:13 +02:00
Thomas Camlong f4a6f37415 style: fix formatting in v2 files 2026-08-13 19:46:00 +02:00
homarr-renovate[bot] 95bfaf5127 chore(deps): update dependency @gfazioli/mantine-onboarding-tour to ^4.0.7 (#6343)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-08-13 19:46:00 +02:00
Thomas 83e3b22f9a feat(custom-widgets): Full Mantine v9 + SubFetch support for custom JSX (#6304) 2026-08-13 19:45:44 +02:00
Thomas cd71404288 Delete .opencode directory 2026-08-13 19:44:18 +02:00
Thomasandcopilot-swe-agent[bot] 7a089ab177 feat: community workshop for custom widgets and CSS (#5984)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-08-13 19:44:18 +02:00
Thomas Camlong 7652eb8c1c preserve smart home toggle response 2026-08-13 17:04:00 +02:00
Thomas Camlong e761c10f32 fix Home Assistant widget actions 2026-08-13 16:21:48 +02:00
Thomas 2513e8454f fix: open user menu instantly (#6568) 2026-08-13 11:44:10 +02:00
ajnart f6137b2dcc fix(truenas): guard empty dataset percentage 2026-08-12 18:24:33 +02:00
ajnart 425dd90871 fix(truenas): report usable dataset space 2026-08-12 18:19:07 +02:00
944 changed files with 33676 additions and 109820 deletions

No files matched your search

@@ -1,6 +1,6 @@
--- ---
description: Comprehensive Homarr codebase architecture and conventions reference name: codebase-context
alwaysApply: true description: Comprehensive Homarr codebase architecture and conventions reference. Use when orienting in the monorepo, understanding package dependency layers, the three runtime services and ports, tRPC router structure, multi-database support, widget/integration/cron system architecture, routing, auth providers, env vars, or key patterns before writing code.
--- ---
# Homarr Codebase Context # Homarr Codebase Context
@@ -27,7 +27,7 @@ Homarr is an open-source, self-hosted dashboard for managing homelab services. I
## Repository Structure ## Repository Structure
``` ```text
homarr/ homarr/
├── apps/ ├── apps/
│ ├── nextjs/ # Main Next.js application (port 3000) │ ├── nextjs/ # Main Next.js application (port 3000)
@@ -66,8 +66,6 @@ homarr/
│ ├── analytics/ # Server-side analytics (Umami) │ ├── analytics/ # Server-side analytics (Umami)
│ ├── server-settings/ # Server setting keys/types │ ├── server-settings/ # Server setting keys/types
│ ├── settings/ # User-facing settings UI context │ ├── settings/ # User-facing settings UI context
│ ├── old-import/ # Legacy Homarr import
│ ├── old-schema/ # Legacy Homarr zod schemas
│ └── cli/ # Node CLI for ops (brocli) │ └── cli/ # Node CLI for ops (brocli)
├── tooling/ ├── tooling/
│ ├── typescript/ # Base tsconfig │ ├── typescript/ # Base tsconfig
@@ -80,7 +78,7 @@ homarr/
## Three Runtime Services ## Three Runtime Services
| Service | Port | Role | | Service | Port | Role |
|---------|------|------| | --------- | ---- | -------------------------------------------- |
| Next.js | 3000 | Main web app (SSR + API routes) | | Next.js | 3000 | Main web app (SSR + API routes) |
| WebSocket | 3001 | tRPC subscriptions via `ws` | | WebSocket | 3001 | tRPC subscriptions via `ws` |
| Tasks | 3002 | Cron job runner + management API via Fastify | | Tasks | 3002 | Cron job runner + management API via Fastify |
@@ -112,6 +110,7 @@ In Docker, nginx listens on **7575** and proxies to all three. The tasks service
### Multi-Database Support ### Multi-Database Support
Drizzle schemas exist in three parallel implementations: Drizzle schemas exist in three parallel implementations:
- `packages/db/schema/sqlite.ts` - `packages/db/schema/sqlite.ts`
- `packages/db/schema/mysql.ts` - `packages/db/schema/mysql.ts`
- `packages/db/schema/postgresql.ts` - `packages/db/schema/postgresql.ts`
@@ -121,6 +120,7 @@ Selected at runtime via `DB_DRIVER` env (`better-sqlite3` | `mysql2` | `node-pos
### Widget System ### Widget System
Widgets are registered in `packages/widgets/src/index.tsx` as `widgetImports` (must satisfy `Record<WidgetKind, ...>`). Each widget: Widgets are registered in `packages/widgets/src/index.tsx` as `widgetImports` (must satisfy `Record<WidgetKind, ...>`). Each widget:
- Has a `definition` created via `createWidgetDefinition(kind, { icon, createOptions, supportedIntegrations? })` - Has a `definition` created via `createWidgetDefinition(kind, { icon, createOptions, supportedIntegrations? })`
- Uses `optionsBuilder.from(factory => ({ ... }))` for typed options - Uses `optionsBuilder.from(factory => ({ ... }))` for typed options
- Loads dynamically via `next/dynamic` - Loads dynamically via `next/dynamic`
@@ -133,6 +133,7 @@ Integrations are defined in `packages/definitions/src/integration.ts` (`integrat
### Cron Job System ### Cron Job System
Jobs defined in `packages/cron-jobs/src/jobs/`. Each job: Jobs defined in `packages/cron-jobs/src/jobs/`. Each job:
- Uses `createCronJob` with a cron expression - Uses `createCronJob` with a cron expression
- Publishes results to Redis channels - Publishes results to Redis channels
- Widget subscriptions consume those channels via tRPC subscriptions - Widget subscriptions consume those channels via tRPC subscriptions
@@ -160,6 +161,7 @@ Configured via `AUTH_PROVIDERS` env. Supports: `credentials` (local), `ldap`, `o
## Environment Variables ## Environment Variables
Key env vars (from `.env.example`): Key env vars (from `.env.example`):
- `DB_DRIVER`: `better-sqlite3` | `mysql2` | `node-postgres` - `DB_DRIVER`: `better-sqlite3` | `mysql2` | `node-postgres`
- `DB_URL`: Database connection string - `DB_URL`: Database connection string
- `AUTH_SECRET`: NextAuth secret - `AUTH_SECRET`: NextAuth secret
@@ -191,16 +193,3 @@ Key env vars (from `.env.example`):
- Drag-and-drop via `@dnd-kit/*` - Drag-and-drop via `@dnd-kit/*`
- `typescript.ignoreBuildErrors: true` in next.config (types checked separately via `typecheck`) - `typescript.ignoreBuildErrors: true` in next.config (types checked separately via `typecheck`)
- `serverExternalPackages`: `dockerode`, `isomorphic-dompurify`, `jsdom` - `serverExternalPackages`: `dockerode`, `isomorphic-dompurify`, `jsdom`
## Documentation (`apps/docs`)
Docusaurus 3 site at `apps/docs/`, package name `@homarr/docs`.
- Run with `pnpm dev:docs` from root
- Build with `pnpm turbo build --filter=@homarr/docs`
- Uses Tailwind CSS 3 for custom pages (not Mantine)
- Imports `@homarr/definitions` for type-safe integration/widget metadata
- When adding or modifying widgets, integrations, cron jobs, API routes, env vars, or user-facing features, you MUST also update corresponding docs in `apps/docs/docs/`
- Integration docs: `apps/docs/docs/integrations/<slug>/index.mdx` + `index.ts`
- Widget docs: `apps/docs/docs/widgets/<slug>/index.mdx` + `index.ts`
- Strict link checking: broken links/anchors cause build failure
@@ -1,3 +1,8 @@
---
name: datatable-migration
description: Playbook for migrating tables from mantine-react-table (MRT) to mantine-datatable. Use when converting any table component from mantine-react-table to mantine-datatable, working on DataTable column/sort/expansion/context-menu code, or implementing responsive column visibility, column persistence, or transparent tables in widgets.
---
# Migrating Tables from mantine-react-table to mantine-datatable # Migrating Tables from mantine-react-table to mantine-datatable
This document captures every pattern, fix, and decision from the downloads widget rework (PR #6430). Use it as a playbook when porting other tables. This document captures every pattern, fix, and decision from the downloads widget rework (PR #6430). Use it as a playbook when porting other tables.
@@ -7,10 +12,12 @@ This document captures every pattern, fix, and decision from the downloads widge
Current `mantine-react-table` consumers that could be migrated: Current `mantine-react-table` consumers that could be migrated:
**Widgets (in `packages/widgets/`):** **Widgets (in `packages/widgets/`):**
- `docker/component.tsx` — Docker container table - `docker/component.tsx` — Docker container table
- `media-server/component.tsx` — Media server sessions - `media-server/component.tsx` — Media server sessions
**Admin pages (in `apps/nextjs/src/app/[locale]/manage/`):** **Admin pages (in `apps/nextjs/src/app/[locale]/manage/`):**
- `tools/docker/docker-table.tsx` - `tools/docker/docker-table.tsx`
- `tools/tasks/_components/tasks-table.tsx` - `tools/tasks/_components/tasks-table.tsx`
- `tools/api/components/api-keys.tsx` - `tools/api/components/api-keys.tsx`
@@ -19,12 +26,12 @@ Current `mantine-react-table` consumers that could be migrated:
- `tools/kubernetes/` — 8 Kubernetes resource tables - `tools/kubernetes/` — 8 Kubernetes resource tables
**Modals:** **Modals:**
- `packages/modals-collection/src/search-engines/request-media-modal.tsx` - `packages/modals-collection/src/search-engines/request-media-modal.tsx`
**Shared hook to remove once all migrated:** **Shared hook to remove once all migrated:**
- `packages/ui/src/hooks/use-translated-mantine-react-table.ts`
--- - `packages/ui/src/hooks/use-translated-mantine-react-table.ts`
## 1. Core Migration: mantine-react-table → mantine-datatable ## 1. Core Migration: mantine-react-table → mantine-datatable
@@ -67,6 +74,7 @@ const columns: DataTableColumn<MyRow>[] = [
``` ```
Key differences: Key differences:
- `accessorKey` → `accessor` - `accessorKey` → `accessor`
- `header` → `title` - `header` → `title`
- `size` → `width` - `size` → `width`
@@ -118,8 +126,6 @@ const sortedData = useMemo(() => {
<DataTable records={sortedData} sortStatus={sortStatus} onSortStatusChange={setSortStatus} /> <DataTable records={sortedData} sortStatus={sortStatus} onSortStatusChange={setSortStatus} />
``` ```
---
## 2. Making Tables Transparent (Widget Context) ## 2. Making Tables Transparent (Widget Context)
Widgets sit inside board cards that have user-controlled opacity. The table must be fully transparent so the card background shows through. mantine-datatable defaults to `var(--mantine-color-body)` which is opaque. Widgets sit inside board cards that have user-controlled opacity. The table must be fully transparent so the card background shows through. mantine-datatable defaults to `var(--mantine-color-body)` which is opaque.
@@ -178,8 +184,6 @@ Create a CSS file alongside the component:
Import it: `import "./styles.css";` and apply via `className="my-table"`. Import it: `import "./styles.css";` and apply via `className="my-table"`.
---
## 3. Responsive Column Visibility ## 3. Responsive Column Visibility
Use a lookup table of width breakpoints instead of media queries. The widget receives `width` as a prop. Use a lookup table of width breakpoints instead of media queries. The widget receives `width` as a prop.
@@ -195,11 +199,38 @@ interface SizeConfig {
} }
const SIZE_BREAKPOINTS: { maxWidth: number; config: SizeConfig }[] = [ const SIZE_BREAKPOINTS: { maxWidth: number; config: SizeConfig }[] = [
{ maxWidth: 300, config: { fontSize: "xs", iconSize: 12, cellPadding: 2, showSpeedColumns: false, showTimeColumn: false, showStateColumn: false } }, {
{ maxWidth: 500, config: { fontSize: "xs", iconSize: 14, cellPadding: 4, showSpeedColumns: false, showTimeColumn: true, showStateColumn: false } }, maxWidth: 300,
config: {
fontSize: "xs",
iconSize: 12,
cellPadding: 2,
showSpeedColumns: false,
showTimeColumn: false,
showStateColumn: false,
},
},
{
maxWidth: 500,
config: {
fontSize: "xs",
iconSize: 14,
cellPadding: 4,
showSpeedColumns: false,
showTimeColumn: true,
showStateColumn: false,
},
},
]; ];
const DEFAULT_SIZE_CONFIG: SizeConfig = { fontSize: "sm", iconSize: 14, cellPadding: 4, showSpeedColumns: true, showTimeColumn: true, showStateColumn: true }; const DEFAULT_SIZE_CONFIG: SizeConfig = {
fontSize: "sm",
iconSize: 14,
cellPadding: 4,
showSpeedColumns: true,
showTimeColumn: true,
showStateColumn: true,
};
function getSizeConfig(width: number): SizeConfig { function getSizeConfig(width: number): SizeConfig {
for (const { maxWidth, config } of SIZE_BREAKPOINTS) { for (const { maxWidth, config } of SIZE_BREAKPOINTS) {
@@ -230,8 +261,6 @@ function shouldIncludeColumn(accessor: string, ctx: ColumnContext): boolean {
} }
``` ```
---
## 4. Column Persistence (Drag/Resize → Server) ## 4. Column Persistence (Drag/Resize → Server)
`useDataTableColumns` stores column order and widths in localStorage by default. For widgets, this must persist server-side so it works across devices. `useDataTableColumns` stores column order and widths in localStorage by default. For widgets, this must persist server-side so it works across devices.
@@ -263,15 +292,6 @@ Even hidden options need translation keys or the translation test fails:
"widget.mywidget.option.columnWidths.label": "Column widths" "widget.mywidget.option.columnWidths.label": "Column widths"
``` ```
### Old-import mappings
Add to `packages/old-import/src/widgets/options.ts`:
```typescript
columnOrder: () => undefined,
columnWidths: () => undefined,
```
### Dynamic storeKey ### Dynamic storeKey
Use a key that includes `itemId` and sorted column names. This prevents cross-widget localStorage bleeding: Use a key that includes `itemId` and sorted column names. This prevents cross-widget localStorage bleeding:
@@ -300,10 +320,15 @@ useEffect(() => {
if (savedOrder.length > 0) setColumnsOrder(savedOrder); if (savedOrder.length > 0) setColumnsOrder(savedOrder);
if (Object.keys(savedWidths).length > 0) { if (Object.keys(savedWidths).length > 0) {
setMultipleColumnWidths( setMultipleColumnWidths(
Object.entries(savedWidths).map(([accessor, w]) => ({ accessor, width: w })), Object.entries(savedWidths).map(([accessor, w]) => ({
accessor,
width: w,
})),
); );
} }
requestAnimationFrame(() => { hydrated.current = true; }); requestAnimationFrame(() => {
hydrated.current = true;
});
}, [storeKey, savedOrder, savedWidths, setColumnsOrder, setMultipleColumnWidths]); }, [storeKey, savedOrder, savedWidths, setColumnsOrder, setMultipleColumnWidths]);
``` ```
@@ -330,9 +355,9 @@ useEffect(() => {
for (const entry of columnsWidth) { for (const entry of columnsWidth) {
const key = Object.keys(entry)[0]; const key = Object.keys(entry)[0];
if (!key) continue; if (!key) continue;
const w = entry[key as keyof typeof entry]; const v = entry[key as keyof typeof entry];
if (typeof w === "number") widthMap[key] = w; if (typeof v === "number") widthMap[key] = v;
else if (typeof w === "string" && w.endsWith("px")) widthMap[key] = parseInt(w, 10); else if (typeof v === "string" && v.endsWith("px")) widthMap[key] = parseInt(v, 10);
} }
persistOption({ columnWidths: JSON.stringify(widthMap) }); persistOption({ columnWidths: JSON.stringify(widthMap) });
} }
@@ -341,8 +366,6 @@ useEffect(() => {
The `persistOption` helper calls both `setOptions` (local state) and `saveItemOptions` (server mutation). The `persistOption` helper calls both `setOptions` (local state) and `saveItemOptions` (server mutation).
---
## 5. Context Menu (Right-Click) ## 5. Context Menu (Right-Click)
Do NOT install `mantine-contextmenu`. Use Mantine's `Menu` + `Portal` directly: Do NOT install `mantine-contextmenu`. Use Mantine's `Menu` + `Portal` directly:
@@ -379,10 +402,24 @@ function RowContextMenu({ state, onClose }: { state: ContextMenuState; onClose:
<Portal> <Portal>
<Menu opened onClose={onClose} closeOnItemClick={false} position="right-start" offset={0}> <Menu opened onClose={onClose} closeOnItemClick={false} position="right-start" offset={0}>
<Menu.Target> <Menu.Target>
<Box style={{ position: "fixed", left: state.x, top: state.y, width: 0, height: 0 }} /> <Box
style={{
position: "fixed",
left: state.x,
top: state.y,
width: 0,
height: 0,
}}
/>
</Menu.Target> </Menu.Target>
<Menu.Dropdown> <Menu.Dropdown>
<Menu.Item onClick={() => { /* action */ onClose(); }}>Action</Menu.Item> <Menu.Item
onClick={() => {
/* action */ onClose();
}}
>
Action
</Menu.Item>
</Menu.Dropdown> </Menu.Dropdown>
</Menu> </Menu>
</Portal> </Portal>
@@ -392,8 +429,6 @@ function RowContextMenu({ state, onClose }: { state: ContextMenuState; onClose:
**Key fix:** Use `closeOnItemClick={false}` if any menu item needs a two-step confirmation (like delete → confirm). Close the menu on scroll: `onScroll={() => { if (contextMenu) closeContextMenu(); }}`. **Key fix:** Use `closeOnItemClick={false}` if any menu item needs a two-step confirmation (like delete → confirm). Close the menu on scroll: `onScroll={() => { if (contextMenu) closeContextMenu(); }}`.
---
## 6. Row Expansion ## 6. Row Expansion
```typescript ```typescript
@@ -413,8 +448,6 @@ function RowContextMenu({ state, onClose }: { state: ContextMenuState; onClose:
**Critical:** Do NOT set `onRowClick` when using `rowExpansion` with `trigger: "click"`. They conflict and the expansion won't work. **Critical:** Do NOT set `onRowClick` when using `rowExpansion` with `trigger: "click"`. They conflict and the expansion won't work.
---
## 7. Common Pitfalls & Fixes ## 7. Common Pitfalls & Fixes
### Status indicator dots ### Status indicator dots
@@ -475,8 +508,11 @@ Always wrap in try/catch:
```typescript ```typescript
function parseJsonOption<T>(value: string | undefined, fallback: T): T { function parseJsonOption<T>(value: string | undefined, fallback: T): T {
if (!value) return fallback; if (!value) return fallback;
try { return JSON.parse(value) as T; } try {
catch { return fallback; } return JSON.parse(value) as T;
} catch {
return fallback;
}
} }
``` ```
@@ -494,20 +530,19 @@ If the widget has configurable default sort, sync it:
```typescript ```typescript
useEffect(() => { useEffect(() => {
setSortStatus({ columnAccessor: options.defaultSort, direction: defaultSortDirection }); setSortStatus({
columnAccessor: options.defaultSort,
direction: defaultSortDirection,
});
}, [options.defaultSort, defaultSortDirection]); }, [options.defaultSort, defaultSortDirection]);
``` ```
---
## 8. Accessibility ## 8. Accessibility
- All `ActionIcon` components need `aria-label` - All `ActionIcon` components need `aria-label`
- Filter toggles (client badges, status badges) need `aria-pressed={isActive}` - Filter toggles (client badges, status badges) need `aria-pressed={isActive}`
- `textSelectionDisabled` on DataTable prevents accidental text selection during drag/resize - `textSelectionDisabled` on DataTable prevents accidental text selection during drag/resize
---
## 9. Error Handling ## 9. Error Handling
- Every mutation needs an `onError` callback with `showErrorNotification` - Every mutation needs an `onError` callback with `showErrorNotification`
@@ -518,12 +553,14 @@ useEffect(() => {
```typescript ```typescript
const mutationOptions = { const mutationOptions = {
onSettled: () => void utils.myQuery.invalidate(), onSettled: () => void utils.myQuery.invalidate(),
onError: () => showErrorNotification({ title: t("errors.actionFailed"), message: t("errors.actionFailedMessage") }), onError: () =>
showErrorNotification({
title: t("errors.actionFailed"),
message: t("errors.actionFailedMessage"),
}),
}; };
``` ```
---
## 10. i18n ## 10. i18n
- No hardcoded strings. Every user-visible string goes through `useScopedI18n` - No hardcoded strings. Every user-visible string goes through `useScopedI18n`
@@ -534,8 +571,6 @@ const mutationOptions = {
- Errors: `t("errors.{errorKey}")` - Errors: `t("errors.{errorKey}")`
- Hidden options still need `.label` translation keys - Hidden options still need `.label` translation keys
---
## 11. Edit Mode ## 11. Edit Mode
Disable pointer events on the table during edit mode to prevent conflicts with the board's drag-and-drop: Disable pointer events on the table during edit mode to prevent conflicts with the board's drag-and-drop:
@@ -550,8 +585,6 @@ if (isEditMode) rowContextMenuHandler = undefined;
<DataTable style={{ pointerEvents: tablePointerEvents }} /> <DataTable style={{ pointerEvents: tablePointerEvents }} />
``` ```
---
## 12. DataTable Props Reference (Commonly Used) ## 12. DataTable Props Reference (Commonly Used)
```typescript ```typescript
@@ -590,8 +623,6 @@ if (isEditMode) rowContextMenuHandler = undefined;
/> />
``` ```
---
## 13. Layout Stability ## 13. Layout Stability
- Don't use `table-layout: fixed` — it fights mantine-datatable's resize logic - Don't use `table-layout: fixed` — it fights mantine-datatable's resize logic
@@ -600,8 +631,6 @@ if (isEditMode) rowContextMenuHandler = undefined;
- Names should truncate at the cell boundary, not a character count, so the layout is stable regardless of content length - Names should truncate at the cell boundary, not a character count, so the layout is stable regardless of content length
- `flexShrink: 0` on badges/icons prevents them from being squeezed by long names - `flexShrink: 0` on badges/icons prevents them from being squeezed by long names
---
## 14. Checklist for Each Migration ## 14. Checklist for Each Migration
1. Replace imports (`mantine-react-table` → `mantine-datatable`) 1. Replace imports (`mantine-react-table` → `mantine-datatable`)
@@ -610,13 +639,12 @@ if (isEditMode) rowContextMenuHandler = undefined;
4. Add CSS file for transparent backgrounds (if widget context) 4. Add CSS file for transparent backgrounds (if widget context)
5. Add responsive column visibility (if widget context) 5. Add responsive column visibility (if widget context)
6. Add hidden options + hydration logic (if column persistence needed) 6. Add hidden options + hydration logic (if column persistence needed)
7. Add old-import mappings for new options 7. Add translation keys (including hidden option labels)
8. Add translation keys (including hidden option labels) 8. Add context menu if row actions exist
9. Add context menu if row actions exist 9. Add row expansion if detail view exists
10. Add row expansion if detail view exists 10. Add error handling on all mutations
11. Add error handling on all mutations 11. Add accessibility attributes
12. Add accessibility attributes 12. Test at multiple widget widths
13. Test at multiple widget widths 13. Update docs in `apps/docs/docs/widgets/` if applicable
14. Update docs in `apps/docs/docs/widgets/` if applicable 14. Run `pnpm turbo typecheck`
15. Run `pnpm turbo typecheck` — catches old-import mismatches 15. Run translation spec — catches missing keys
16. Run translation spec — catches missing keys
@@ -0,0 +1,44 @@
---
name: documentation-sync
description: Keep Homarr documentation in sync with code changes. Use when modifying code that affects user-facing behavior, or when adding/modifying widgets, integrations, cron jobs, API routes, env vars, permissions, auth providers, or UI in apps/docs. Covers the docs/ mapping table, integration doc pattern, and widget doc pattern.
---
# Documentation Sync
When modifying code that affects user-facing behavior, you MUST also update the documentation in `apps/docs/`.
## What triggers a doc update
| Code change | Doc location |
| ---------------------- | ----------------------------------------------------------- |
| New integration | `apps/docs/docs/integrations/<slug>/index.mdx` + `index.ts` |
| New widget | `apps/docs/docs/widgets/<slug>/index.mdx` + `index.ts` |
| Changed API surface | `apps/docs/docs/management/api.mdx` |
| New/changed env vars | `apps/docs/docs/advanced/` |
| New CLI commands | `apps/docs/docs/advanced/command-line/` |
| Changed auth providers | `apps/docs/docs/advanced/` SSO pages |
| UI/UX changes | Relevant getting-started or management pages |
| New cron job | `apps/docs/docs/management/tasks.mdx` |
| Changed permissions | `apps/docs/docs/management/users.mdx` |
## Integration doc pattern
Each integration has a folder under `apps/docs/docs/integrations/<slug>/`:
- `index.ts` — exports a typed `IntegrationDefinition` with name, description, iconUrl, path
- `index.mdx` — uses shared components: `IntegrationHeader`, `IntegrationCapabilites`, `IntegrationSecrets`
- Import metadata from `@homarr/definitions` where possible
## Widget doc pattern
Each widget has a folder under `apps/docs/docs/widgets/<slug>/`:
- `index.ts` — exports a typed `WidgetDefinition` with icon, name, description, path, configuration
- `index.mdx` — uses shared components: `WidgetHeader`, `WidgetConfig`, `WidgetAdding`
## Development
- Run docs locally: `pnpm dev:docs` from root
- Build docs: `pnpm turbo build --filter=@homarr/docs`
- The docs app can import from `@homarr/definitions` for type-safe integration/widget metadata
- Docusaurus strict mode throws on broken links — fix any broken references before committing
+1 -9
View File
@@ -7,15 +7,11 @@ description: Author safe Homarr Custom JSX v2 widgets from API documentation.
Author one widget at a time. When connected to Homarr, retrieve `homarr://custom-widgets/schema` and only the component resources needed for the design. The live resources are authoritative for the installed release. Author one widget at a time. When connected to Homarr, retrieve `homarr://custom-widgets/schema` and only the component resources needed for the design. The live resources are authoritative for the installed release.
Keep documentation retrieval targeted: fetch at most eight named component documents before the first validation, or at most four after loading a complete example. Fetch another component only when a validation issue or a concrete missing interaction requires it; never load the catalog component-by-component.
1. Read the API documentation. 1. Read the API documentation.
2. Create one credential-free widget with keyed `sources`, keyed `requests`, optional keyed `options`, and safe JSX `template`. 2. Create one credential-free widget with keyed `sources`, keyed `requests`, optional keyed `options`, and safe JSX `template`.
3. Validate, preview, test queries and simulated actions, visually inspect, and iterate. 3. Validate, preview, test queries and simulated actions, visually inspect, and iterate.
4. Configure deployment-specific source URLs and request credentials through Homarr; never repeat plaintext. 4. Configure deployment-specific source URLs and request credentials through Homarr; never repeat plaintext.
5. Save only after narrow/wide, light/dark, loading, empty, error, and success states work. When connected through MCP, persist the exact final tested preview with `customWidget_createFromPreview`; do not resend a large template through `customWidget_create` unless no preview session is available. 5. Save only after narrow/wide, light/dark, loading, empty, error, and success states work.
Treat a supplied sample or successful preview response as the binding contract. Render every core requested field, guard optional arrays and nested values before indexing, and do not silently drop returned items. Show concise freshness context when the API supplies a timestamp. Give recoverable load errors and empty states a clear refresh or retry path.
Use `{option:name}` or `$option` for saved options. Use `{param:name}` or `$param` for values supplied by `SubFetch`, `ActionButton`, or `ToggleSwitch`. Load queries cannot use invocation parameters. Templates read `data`, `status`, `options`, and temporary `inputs`. Use `{option:name}` or `$option` for saved options. Use `{param:name}` or `$param` for values supplied by `SubFetch`, `ActionButton`, or `ToggleSwitch`. Load queries cannot use invocation parameters. Templates read `data`, `status`, `options`, and temporary `inputs`.
@@ -23,10 +19,6 @@ Use `{option:name}` or `$option` for saved options. Use `{param:name}` or `$para
Use standard Mantine compound names and deliberate visual hierarchy, spacing, restrained semantic color, and responsive layouts. Avoid excessive nested cards. Use standard Mantine compound names and deliberate visual hierarchy, spacing, restrained semantic color, and responsive layouts. Avoid excessive nested cards.
Aim for distinctive polish without decorative gradients or fragile custom styling: use one semantic accent, clear surface contrast, purposeful typography, and compact rows that remain scannable in narrow tiles. Give standalone icons an accessible label or pair them with visible text.
Make initial states actionable with an example, useful hint, or clear next step. Use wrapping groups for variable-length labels and values on narrow tiles. Do not use an unlabeled decorative icon as an empty state.
Do not use imports, hooks, refs, raw HTML, raw event callbacks, browser requests, arbitrary functions, eval, bigint, npm packages, authored statement blocks, IIFEs, or recursion. Do not pretend MCP tools exist in an offline chat session. Do not use imports, hooks, refs, raw HTML, raw event callbacks, browser requests, arbitrary functions, eval, bigint, npm packages, authored statement blocks, IIFEs, or recursion. Do not pretend MCP tools exist in an offline chat session.
To install an existing widget, use `customWidget_workshopSearch`, inspect it with `customWidget_workshopGet`, then use `customWidget_workshopInstall`. Configure each self-hosted source afterward with `customWidget_sourceConfigure`, or call `customWidget_configurationRequestUser` so the user can enter the server URL and credentials directly in Homarr. To install an existing widget, use `customWidget_workshopSearch`, inspect it with `customWidget_workshopGet`, then use `customWidget_workshopInstall`. Configure each self-hosted source afterward with `customWidget_sourceConfigure`, or call `customWidget_configurationRequestUser` so the user can enter the server URL and credentials directly in Homarr.
@@ -1,13 +1,14 @@
--- ---
description: How to expose tRPC procedures as MCP tools for AI clients name: mcp-integration
globs: packages/api/src/router/**/*.ts description: Expose tRPC procedures as MCP tools for AI clients. Use when modifying procedures in packages/api/src/router, adding a new tRPC procedure or router, or deciding what to expose via Homarr's MCP interface. Covers .meta({ mcp }), router registration in packages/api/src/mcp.ts, input schema rules, and what to expose vs skip.
alwaysApply: false
--- ---
# MCP Integration Guide # MCP Integration
Every new tRPC procedure that provides useful functionality should be exposed as an MCP tool so AI assistants (Claude, Cursor, etc.) can use it. This is a first-class feature of Homarr. Every new tRPC procedure that provides useful functionality should be exposed as an MCP tool so AI assistants (Claude, Cursor, etc.) can use it. This is a first-class feature of Homarr.
Exposing a procedure makes it callable by any AI client with an API key. Before adding `.meta({ mcp })`, review what the tool exposes: authorization requirements, sensitive data, tenant isolation, auditability, and whether the action is destructive. Prefer exposing read-only queries over mutations unless the mutation is safe and permission-scoped. When in doubt, ask for a security review before shipping a new MCP tool.
## Adding MCP to a procedure ## Adding MCP to a procedure
Add `.meta({ mcp: { enabled: true, description: "..." } })` to the procedure chain: Add `.meta({ mcp: { enabled: true, description: "..." } })` to the procedure chain:
@@ -16,7 +17,7 @@ Add `.meta({ mcp: { enabled: true, description: "..." } })` to the procedure cha
// Read-only tool (query) // Read-only tool (query)
myProcedure: protectedProcedure myProcedure: protectedProcedure
.meta({ mcp: { enabled: true, description: "Get all items with their status and metadata" } }) .meta({ mcp: { enabled: true, description: "Get all items with their status and metadata" } })
.input(z.object({ limit: z.number().default(50) })) .input(z.object({ limit: z.number().default(10) }))
.query(async ({ ctx, input }) => { ... }) .query(async ({ ctx, input }) => { ... })
// Write tool (mutation) // Write tool (mutation)
@@ -38,23 +39,27 @@ The `.meta()` call must come **before** `.input()` and `.query()`/`.mutation()`
The description is the **only thing** an AI sees to decide when and how to use the tool. Write it like you're explaining to a colleague who has never used Homarr. The description is the **only thing** an AI sees to decide when and how to use the tool. Write it like you're explaining to a colleague who has never used Homarr.
**Must include:** **Must include:**
- What the tool returns or does - What the tool returns or does
- Which integrations/services it works with (if applicable) - Which integrations/services it works with (if applicable)
- How to get required IDs (e.g., "Use integration_all to get the integrationId") - How to get required IDs (e.g., "Use integration_all to get the integrationId")
- Permission requirements (if non-obvious) - Permission requirements (if non-obvious)
**Good:** **Good:**
```
```text
"Get calendar events for upcoming and recent media releases. Fetches from all connected Sonarr (TV), Radarr (movies), Lidarr (music), and Readarr (books) integrations. Requires integrationIds from integration_all" "Get calendar events for upcoming and recent media releases. Fetches from all connected Sonarr (TV), Radarr (movies), Lidarr (music), and Readarr (books) integrations. Requires integrationIds from integration_all"
``` ```
**Bad:** **Bad:**
```
```text
"Get calendar events" "Get calendar events"
``` ```
**For tools that return permission fields**, explain what they mean: **For tools that return permission fields**, explain what they mean:
```
```text
"List all integrations. Returns permissions.hasUseAccess (read) and permissions.hasInteractAccess (actions) — false means the API key lacks that permission, not an error" "List all integrations. Returns permissions.hasUseAccess (read) and permissions.hasInteractAccess (actions) — false means the API key lacks that permission, not an error"
``` ```
@@ -77,11 +82,13 @@ export const mcpRouter = createTRPCRouter({
## What to expose vs. what to skip ## What to expose vs. what to skip
**Expose:** **Expose:**
- Data queries (list, search, get by ID) - Data queries (list, search, get by ID)
- Actions users would ask an AI to do (toggle, create, delete, approve) - Actions users would ask an AI to do (toggle, create, delete, approve)
- Status checks (health, stats, summaries) - Status checks (health, stats, summaries)
**Skip:** **Skip:**
- Subscriptions (WebSocket-only, not supported by MCP) - Subscriptions (WebSocket-only, not supported by MCP)
- Internal procedures (session management, onboarding steps) - Internal procedures (session management, onboarding steps)
- File upload/download procedures - File upload/download procedures
+1
View File
@@ -0,0 +1 @@
../.agents/skills
-50
View File
@@ -1,50 +0,0 @@
---
description: Keep documentation in sync with code changes
globs:
- packages/definitions/src/**
- packages/widgets/src/**
- packages/integrations/src/**
- packages/api/src/**
- apps/nextjs/src/**
- apps/docs/**
---
# Documentation Sync Rule
When modifying code that affects user-facing behavior, you MUST also update the documentation in `apps/docs/`.
## What triggers a doc update
| Code change | Doc location |
|---|---|
| New integration | `apps/docs/docs/integrations/<slug>/index.mdx` + `index.ts` |
| New widget | `apps/docs/docs/widgets/<slug>/index.mdx` + `index.ts` |
| Changed API surface | `apps/docs/docs/management/api.mdx` |
| New/changed env vars | `apps/docs/docs/advanced/` |
| New CLI commands | `apps/docs/docs/advanced/command-line/` |
| Changed auth providers | `apps/docs/docs/advanced/` SSO pages |
| UI/UX changes | Relevant getting-started or management pages |
| New cron job | `apps/docs/docs/management/tasks.mdx` |
| Changed permissions | `apps/docs/docs/management/users.mdx` |
## Integration doc pattern
Each integration has a folder under `apps/docs/docs/integrations/<slug>/`:
- `index.ts` — exports a typed `IntegrationDefinition` with name, description, iconUrl, path
- `index.mdx` — uses shared components: `IntegrationHeader`, `IntegrationCapabilites`, `IntegrationSecrets`
- Import metadata from `@homarr/definitions` where possible
## Widget doc pattern
Each widget has a folder under `apps/docs/docs/widgets/<slug>/`:
- `index.ts` — exports a typed `WidgetDefinition` with icon, name, description, path, configuration
- `index.mdx` — uses shared components: `WidgetHeader`, `WidgetConfig`, `WidgetAdding`
## Development
- Run docs locally: `pnpm dev:docs` from root
- Build docs: `pnpm turbo build --filter=@homarr/docs`
- The docs app can import from `@homarr/definitions` for type-safe integration/widget metadata
- Docusaurus strict mode throws on broken links — fix any broken references before committing
+5
View File
@@ -33,6 +33,11 @@ body:
options: options:
# The below comment is used to insert a new version with on-release.yml # The below comment is used to insert a new version with on-release.yml
#NEXT_VERSION# #NEXT_VERSION#
- 1.77.0
- 1.76.2
- 1.76.1
- 1.76.0
- 1.75.0
- 1.74.0 - 1.74.0
- 1.73.0 - 1.73.0
- 1.72.0 - 1.72.0
@@ -68,8 +68,6 @@ jobs:
with: with:
node-version: 24.18.0 node-version: 24.18.0
package-manager-cache: false package-manager-cache: false
- run: npm i -g pnpm
if: env.SKIP_RELEASE == 'false'
- name: Install dependencies - name: Install dependencies
if: env.SKIP_RELEASE == 'false' if: env.SKIP_RELEASE == 'false'
run: | run: |
-3
View File
@@ -74,9 +74,6 @@ apps/nextjs/public/images/background.png
.opencode .opencode
.agents/* .agents/*
!.agents/skills/ !.agents/skills/
.agents/skills/*
!.agents/skills/homarr-custom-widget/
!.agents/skills/homarr-custom-widget/**
skills-lock.json skills-lock.json
# docusaurus # docusaurus
+8
View File
@@ -0,0 +1,8 @@
{
"mcpServers": {
"mantine": {
"command": "npx",
"args": ["-y", "@mantine/mcp-server"]
}
}
}
+10
View File
@@ -84,6 +84,16 @@ When modifying user-facing code, you MUST also update the corresponding document
- Lint: oxlint (not ESLint) - Lint: oxlint (not ESLint)
- Format: oxfmt (not Prettier) - Format: oxfmt (not Prettier)
- UI: Mantine (not Tailwind) — Tailwind is only used in docs app - UI: Mantine (not Tailwind) — Tailwind is only used in docs app
- Mantine: use the `mantine` MCP server (configured in `.mcp.json`; tools `get_item_doc` / `get_item_props` / `search_docs`) for current v9 APIs before writing component code. Prefer built-in primitives (`Combobox`/`useCombobox` for selects, the polymorphic `component` prop, `@mantine/hooks`) over hand-rolled equivalents. Check `packages/ui/` for existing conventions first.
## MCP servers
`.mcp.json` at the repo root declares project-scoped MCP servers (the standard Claude Code layout; other tools can import the same `mcpServers` block). See the `mantine` entry there for the Mantine v9 server, and the upstream docs at https://mantine.dev/guides/llms/ for per-tool setup (OpenCode: `.opencode/opencode.json`, Codex: `.codex/config.toml`). Only keyless server definitions belong in `.mcp.json`; anything needing secrets stays in local dotfiles.
## Agent Skills
Portable agent skills live in `.agents/skills/` (the Agent Skills open standard). Cursor, Codex, and OpenCode read `.agents/skills/` natively. Claude Code does not scan it, so `.claude/skills/` is a symlink back to `.agents/skills/`. Install skills at `.agents/skills/<name>/SKILL.md` — that single copy is used everywhere. See `.agents/skills/` for available skills (documentation-sync, mcp-integration, codebase-context, datatable-migration).
- Icons: @tabler/icons-react - Icons: @tabler/icons-react
- Docs app can import from `@homarr/definitions` for shared types - Docs app can import from `@homarr/definitions` for shared types
- Run `pnpm dev:cli -- dev` to browse local `homarr:*` images and remote PR images. - Run `pnpm dev:cli -- dev` to browse local `homarr:*` images and remote PR images.
+10
View File
@@ -1,3 +1,13 @@
## [1.77.0](https://github.com/homarr-labs/homarr/compare/v1.76.2...v1.77.0) (2026-09-06)
## [1.76.2](https://github.com/homarr-labs/homarr/compare/v1.76.1...v1.76.2) (2026-08-27)
## [1.76.1](https://github.com/homarr-labs/homarr/compare/v1.76.0...v1.76.1) (2026-08-26)
## [1.76.0](https://github.com/homarr-labs/homarr/compare/v1.75.0...v1.76.0) (2026-08-21)
## [1.75.0](https://github.com/homarr-labs/homarr/compare/v1.74.0...v1.75.0) (2026-08-14)
## [1.74.0](https://github.com/homarr-labs/homarr/compare/v1.73.0...v1.74.0) (2026-08-07) ## [1.74.0](https://github.com/homarr-labs/homarr/compare/v1.73.0...v1.74.0) (2026-08-07)
## [1.73.0](https://github.com/homarr-labs/homarr/compare/v1.72.0...v1.73.0) (2026-07-31) ## [1.73.0](https://github.com/homarr-labs/homarr/compare/v1.72.0...v1.73.0) (2026-07-31)
-475
View File
@@ -1,475 +0,0 @@
# Mantine UI - LLM Documentation
This index lists Mantine documentation pages formatted for LLMs.
Each link points to a standalone Markdown file under the /llms path.
For a single consolidated file with all content, use:
- https://mantine.dev/llms-full.txt
## FAQ
- [Are Mantine components accessible?](https://mantine.dev/llms/q-are-mantine-components-accessible.md): Learn about Mantine components accessibility features
- [Browser zooms in when input is focused. What should I do?](https://mantine.dev/llms/q-browser-zooms-on-focus.md): Use meta tag to disable browser scaling or increase input size
- [Can I have color schemes other than light and dark?](https://mantine.dev/llms/q-light-dark-is-not-enough.md): Learn about the difference between color scheme and theme
- [Can I have different primary color for light and dark color schemes?](https://mantine.dev/llms/q-primary-virtual-color.md): Learn how to use virtual color with primary color in theme object
- [Can I remove MultiSelect placeholder when the component has selected values?](https://mantine.dev/llms/q-multiselect-value-placeholder.md): Learn why MultiSelect placeholder is not removed when values are selected and how to remove it with CSS
- [Can I use an array of strings as a list in use-form?](https://mantine.dev/llms/q-list-of-strings-in-use-form.md): Learn about use-form lists limitations
- [Can I use Mantine components as server components?](https://mantine.dev/llms/q-server-components.md): Learn about use client directive and server components usage
- [Can I use Mantine with Astro?](https://mantine.dev/llms/q-can-i-use-mantine-with-astro.md): No, Astro does not support React context
- [Can I use Mantine with Create React App (CRA)?](https://mantine.dev/llms/q-can-i-use-mantine-with-cra.md): Learn how to use Mantine without postcss-preset-mantine or how to eject CRA and customize webpack.config.js and use postcss-preset-mantine
- [Can I use Mantine with Emotion/styled-components/tailwindcss?](https://mantine.dev/llms/q-third-party-styles.md): Learn about limitations of third-party styles
- [Can I use Mantine with Vue/Svelte/Angular/etc.?](https://mantine.dev/llms/q-vue-svelte-angular.md): No, Mantine is a React library and does not support other frameworks/libraries
- [Can I use nested inline styles with Mantine components?](https://mantine.dev/llms/q-nested-inline-styles.md): Nested styles are supported only in CSS files
- [Can I use PostCSS function in inline styles?](https://mantine.dev/llms/q-postcss-fns-inline.md): Learn where PostCSS functions can be used in Mantine
- [Can I use private CSS variables to style components?](https://mantine.dev/llms/q-private-css-variables.md): No, it is not safe and will not work with future versions of Mantine.
- [Can I use SegmentedControl with empty value?](https://mantine.dev/llms/q-segmented-control-no-value.md): SegmentedControl cannot be used without a value
- [How can I add dynamic CSS styles?](https://mantine.dev/llms/q-dynamic-css-styles.md): Use data attributes, CSS variables or inline styles
- [How can I add fuzzy search to Select component?](https://mantine.dev/llms/q-select-fuzzy.md): Learn how to integrate third-party fuzzy search libraries with Mantine Select component
- [How can I add hover styles to an element?](https://mantine.dev/llms/q-how-to-add-hover-styles.md): Learn how to add hover classes to an element with CSS modules, &:hover or @mixin hover
- [How can I apply styles to all Mantine components?](https://mantine.dev/llms/q-apply-styles-to-all.md): Learn how to use attribute selector to apply styles to all Mantine components
- [How can I change body background color?](https://mantine.dev/llms/q-body-background.md): Use CSS to change body background color
- [How can I change component color prop value depending on the color scheme?](https://mantine.dev/llms/q-color-scheme-color.md): Learn how to use CSS variables resolver to change color value depending on the color scheme
- [How can I change input focus styles?](https://mantine.dev/llms/q-input-focus-styles.md): Learn how to use Styles API with Mantine inputs
- [How can I change Tabs border color?](https://mantine.dev/llms/q-tabs-border-color.md): Learn how to use Styles API with Tabs component
- [How can I contribute to the library?](https://mantine.dev/llms/q-how-can-i-contribute.md): There are multiple ways to contribute even without writing code
- [How can I disable all inputs/input groups inside a form?](https://mantine.dev/llms/q-disable-all-inputs-in-form.md): Learn how to disable all inputs/input groups inside a form with the unstyled Fieldset component
- [How can I display different elements in light and dark color schemes?](https://mantine.dev/llms/q-light-dark-elements.md): Learn how to hide/show elements based on color scheme
- [How can I focus the first input with error with use-form?](https://mantine.dev/llms/q-focus-first-input-with-error.md): Learn how to handle focus with use-form hook
- [How can I get current color scheme value in JavaScript?](https://mantine.dev/llms/q-how-to-get-color-scheme-value-in-js.md): How to use useMantineColorScheme and useComputedColorScheme hooks to get current color scheme value in JavaScript
- [How can I lint CSS files?](https://mantine.dev/llms/q-how-to-setup-stylelint.md): Learn how to set up Stylelint for Mantine postcss syntax
- [How can I load fonts in Next.js?](https://mantine.dev/llms/q-next-load-fonts.md): A guide to load custom fonts in Next.js with CSS and next/font package
- [How can I load fonts in React Router?](https://mantine.dev/llms/q-react-router-load-fonts.md): A guide to load custom fonts in React Router
- [How can I load fonts in Vite?](https://mantine.dev/llms/q-vite-load-fonts.md): A guide to load custom fonts in Vite
- [How can I lock scroll in my application?](https://mantine.dev/llms/q-how-to-lock-scroll.md): Use react-remove-scroll library to lock scroll in your application
- [How can I submit a template to Mantine documentation?](https://mantine.dev/llms/q-submit-template.md): Learn how to create and submit a template to Mantine documentation
- [How can I test Modal/Drawer/Popover components?](https://mantine.dev/llms/q-portals-testing.md): Learn how to use react-testing-library to test components that use portals and transitions.
- [How can I test Select/MultiSelect components?](https://mantine.dev/llms/q-combobox-testing.md): Learn how to use react-testing-library to test Select and MultiSelect components.
- [How can I upload files from Dropzone component?](https://mantine.dev/llms/q-dropzone-upload.md): Learn how to process files dropped into Dropzone component
- [How is that thing done on the mantine.dev website?](https://mantine.dev/llms/q-how-that-thing-is-done.md): Learn how various elements are implemented on the mantine.dev website
- [How to align input with a button in a flex container?](https://mantine.dev/llms/q-align-input-button.md): Learn how to align Mantine inputs with buttons using Group component
- [How to call a function when Modal/Drawer closes and animation completes?](https://mantine.dev/llms/q-how-to-call-function-when-modal-closes.md): How to use transitionProps in Modal/Drawer components
- [How to change background color for FiveM?](https://mantine.dev/llms/q-fivem-background-color.md): Unset body background color and color-scheme styles for FiveM
- [How to change input placeholder color?](https://mantine.dev/llms/q-inputs-placeholder-color.md): Learn how to change placeholder color with Styles API
- [How to integrate custom input with use-form hook?](https://mantine.dev/llms/q-custom-input-use-form.md): Learn how to add use-form support for custom inputs
- [How to prevent Modal from closing?](https://mantine.dev/llms/q-how-to-prevent-modal-from-closing.md): Learn how to prevent Modal from closing when the user clicks outside of it or presses the Escape key
- [How to scroll to the top of the form if the form is submitted with errors?](https://mantine.dev/llms/q-scroll-to-the-top-of-the-form.md): Learn how to call a function when the form is submitted with errors
- [How to update Mantine dependencies?](https://mantine.dev/llms/q-how-to-update-dependencies.md): Learn how to update @mantine/_, @mantinex/_ and postcss-preset-mantine dependencies manually or with ncu script
- [How to use Dropzone with @mantine/form?](https://mantine.dev/llms/q-how-to-use-dropzone-with-form.md): Learn how to use Dropzone with @mantine/form to handle file selection state
- [How to use Mantine template on GitHub?](https://mantine.dev/llms/q-templates-usage.md): Learn how to create a new repository based on Mantine template
- [I get hydration warning about data-mantine-color-scheme attribute, what does it mean?](https://mantine.dev/llms/q-color-scheme-hydration-warning.md): Learn how hydration works and how to fix warnings
- [Is there a comparison with other libraries?](https://mantine.dev/llms/q-other-libs.md): Comparison with other libraries is not provided, you are trusted to make your own decision based on documentation examples
- [Is there a floating action button component?](https://mantine.dev/llms/q-floating-action-button.md): No, but you can build it with Affix and ActionIcon components
- [Is there DataGrid component that I can use with Mantine?](https://mantine.dev/llms/q-data-grid-i-need.md): A list of community packages that provide DataGrid component that can be used with Mantine.
- [It is not possible to pinch to zoom when a Modal is opened. What should I do?](https://mantine.dev/llms/q-pinch-to-zoom-modal.md): Use removeScrollProps to configure react-remove-scroll options
- [MantineProvider was not found in component tree. What should I do?](https://mantine.dev/llms/q-mantine-provider-missing.md): Learn how to resolve MantineProvider issues
- [My buttons are transparent and the background is visible only on hover, what is wrong?](https://mantine.dev/llms/q-transparent-buttons.md): You have installed a third-party library that overrides Mantine styles
- [My Popover dropdown closes when I click on the dropdown of a nested Popover](https://mantine.dev/llms/q-nested-popover-closes.md): Popover dropdown is closed when it detects click-outside events
- [My styles are broken with disabled JavaScript. What should I do?](https://mantine.dev/llms/q-disabled-js.md): Learn how to use Mantine without JavaScript enabled
- [My styles are overridden by Mantine components styles, what should I do?](https://mantine.dev/llms/q-styles-order.md): Learn how to use CSS layers to control styles order and prevent Mantine components from overriding your styles
- [Native browser validation does not work in some components, what should I do?](https://mantine.dev/llms/q-native-required.md): Learn why native browser validation does not work in some components
- [The use-local-storage hook returns the real value only after mounting. Is it a bug?](https://mantine.dev/llms/q-local-storage-effect.md): Learn how Mantine retrieves local storage value
- [What is the difference between searchable Select and Autocomplete?](https://mantine.dev/llms/q-select-autocomplete-difference.md): Searchable Select and Autocomplete are similar components, but they serve different purposes.
- [Where can I find the roadmap?](https://mantine.dev/llms/q-roadmap.md): The roadmap is private and only available to the team.
- [Why are my Carousel slides in vertical orientation?](https://mantine.dev/llms/q-carousel-missing-styles.md): You forgot to import carousel styles
- [Why are my notifications displayed at the wrong position?](https://mantine.dev/llms/q-notifications-missing-styles.md): Because you did not import styles
- [Why can I not use one polymorphic component in the component prop of another polymorphic component?](https://mantine.dev/llms/q-polymorphic-in-polymorphic.md): Learn how polymorphic components types work
- [Why can I not use value/label data structure with Autocomplete/TagsInput?](https://mantine.dev/llms/q-autocomplete-value-label.md): Learn the difference between Autocomplete and Select components
- [Why can VSCode not autoimport the Text component?](https://mantine.dev/llms/q-why-vscode-cannot-autoimport-text.md): It is confused by the native Text constructor
- [Why do I see color scheme flickering on page load?](https://mantine.dev/llms/q-color-scheme-flickering.md): Color scheme flickering is caused by incorrect usage of ColorSchemeScript
- [Why do my dates components look broken?](https://mantine.dev/llms/q-dates-missing-styles.md): Because you did not import styles
- [Why does FileButton not work in Menu?](https://mantine.dev/llms/q-file-button-in-menu.md): Learn how to use FileButton in Menu component
- [Why is it required to have 10 shades per color?](https://mantine.dev/llms/q-ten-shades-per-color.md): Learn how Mantine uses theme colors
- [Why is my screen completely empty after I](https://mantine.dev/llms/q-notifications-empty-screen.md): You have used Notifications component incorrectly
## Core
- [Accordion](https://mantine.dev/llms/core-accordion.md): Divide content into collapsible sections
- [ActionIcon](https://mantine.dev/llms/core-action-icon.md): Icon button
- [Affix](https://mantine.dev/llms/core-affix.md): Renders children inside portal at fixed position
- [Alert](https://mantine.dev/llms/core-alert.md): Attract user attention with important static message
- [AlphaSlider](https://mantine.dev/llms/core-alpha-slider.md): Slider component for selecting alpha channel in color pickers (0 – 1)
- [Anchor](https://mantine.dev/llms/core-anchor.md): Display link with theme styles
- [AngleSlider](https://mantine.dev/llms/core-angle-slider.md): Pick angle value between 0 and 360
- [AppShell](https://mantine.dev/llms/core-app-shell.md): Responsive shell for your application with header, navbar, aside and footer
- [AspectRatio](https://mantine.dev/llms/core-aspect-ratio.md): Maintain responsive consistent width/height ratio
- [Autocomplete](https://mantine.dev/llms/core-autocomplete.md): Autocomplete user input with any list of options
- [Avatar](https://mantine.dev/llms/core-avatar.md): Display user profile image, initials or fallback icon
- [BackgroundImage](https://mantine.dev/llms/core-background-image.md): Displays image as background
- [Badge](https://mantine.dev/llms/core-badge.md): Display badge, pill or tag
- [Blockquote](https://mantine.dev/llms/core-blockquote.md): Blockquote with optional cite
- [Box](https://mantine.dev/llms/core-box.md): Base component for all Mantine components
- [Breadcrumbs](https://mantine.dev/llms/core-breadcrumbs.md): Separates list of react nodes with given separator
- [Burger](https://mantine.dev/llms/core-burger.md): Open/close navigation button
- [Button](https://mantine.dev/llms/core-button.md): Button component to render button or link
- [Card](https://mantine.dev/llms/core-card.md): Card with sections
- [Cascader](https://mantine.dev/llms/core-cascader.md): Select a path from hierarchical data with cascading columns
- [Center](https://mantine.dev/llms/core-center.md): Centers content vertically and horizontally
- [Checkbox](https://mantine.dev/llms/core-checkbox.md): Capture boolean input from user
- [Chip](https://mantine.dev/llms/core-chip.md): Pick one or multiple values with inline controls
- [CloseButton](https://mantine.dev/llms/core-close-button.md): Button with close icon
- [Code](https://mantine.dev/llms/core-code.md): Inline and block code
- [Collapse](https://mantine.dev/llms/core-collapse.md): Animate presence with slide down/up transition
- [ColorInput](https://mantine.dev/llms/core-color-input.md): Capture color from user
- [ColorPicker](https://mantine.dev/llms/core-color-picker.md): Pick colors in hex(a), rgb(a), hsl(a) and hsv(a) formats
- [ColorSwatch](https://mantine.dev/llms/core-color-swatch.md): Displays color
- [Combobox](https://mantine.dev/llms/core-combobox.md): Create custom select, autocomplete or multiselect inputs
- [ComboboxPopover](https://mantine.dev/llms/core-combobox-popover.md): Add combobox dropdown to any button element
- [Container](https://mantine.dev/llms/core-container.md): Center content with padding and max-width
- [CopyButton](https://mantine.dev/llms/core-copy-button.md): Copies given text to clipboard
- [CorePackage](https://mantine.dev/llms/core-package.md)
- [DataList](https://mantine.dev/llms/core-data-list.md): Display label-value pairs as a description list
- [Dialog](https://mantine.dev/llms/core-dialog.md): Display a fixed overlay dialog at any side of the screen
- [Divider](https://mantine.dev/llms/core-divider.md): Horizontal line with optional label or vertical divider
- [Drawer](https://mantine.dev/llms/core-drawer.md): Display overlay area at any side of the screen
- [EmptyState](https://mantine.dev/llms/core-empty-state.md): Display a placeholder for no data, empty results and first-run states
- [Fieldset](https://mantine.dev/llms/core-fieldset.md): Group related elements in a form
- [FileButton](https://mantine.dev/llms/core-file-button.md): Open file picker with a button click
- [FileInput](https://mantine.dev/llms/core-file-input.md): Capture files from user
- [Flex](https://mantine.dev/llms/core-flex.md): Compose elements in a flex container
- [FloatingIndicator](https://mantine.dev/llms/core-floating-indicator.md): Display a floating indicator over a group of elements
- [FloatingWindow](https://mantine.dev/llms/core-floating-window.md): Draggable floating area
- [FocusTrap](https://mantine.dev/llms/core-focus-trap.md): Trap focus at child node
- [Grid](https://mantine.dev/llms/core-grid.md): Responsive 12 columns grid system
- [Group](https://mantine.dev/llms/core-group.md): Compose elements and components in a horizontal flex container
- [Highlight](https://mantine.dev/llms/core-highlight.md): Highlight given part of a string with mark
- [HoverCard](https://mantine.dev/llms/core-hover-card.md): Display popover section when target element is hovered
- [HueSlider](https://mantine.dev/llms/core-hue-slider.md): Slider component for selecting hue channel in color pickers (0 – 360 degrees)
- [Image](https://mantine.dev/llms/core-image.md): Image with optional fallback
- [Indicator](https://mantine.dev/llms/core-indicator.md): Display element at the corner of another element
- [Input](https://mantine.dev/llms/core-input.md): Base component to create custom inputs
- [JsonInput](https://mantine.dev/llms/core-json-input.md): Capture json data from user
- [Kbd](https://mantine.dev/llms/core-kbd.md): Display keyboard key
- [List](https://mantine.dev/llms/core-list.md): Display ordered or unordered list
- [Loader](https://mantine.dev/llms/core-loader.md): Indicate loading state
- [LoadingOverlay](https://mantine.dev/llms/core-loading-overlay.md): An overlay with centered loader
- [Mark](https://mantine.dev/llms/core-mark.md): Highlight part of the text
- [Marquee](https://mantine.dev/llms/core-marquee.md): Create continuous scrolling animation for content
- [MaskInput](https://mantine.dev/llms/core-mask-input.md): Input with mask pattern for formatted text entry
- [Menu](https://mantine.dev/llms/core-menu.md): Combine a list of secondary actions into single interactive area
- [Menubar](https://mantine.dev/llms/core-menubar.md): Desktop application style menubar with a row of menu triggers
- [Modal](https://mantine.dev/llms/core-modal.md): An accessible overlay dialog
- [MultiSelect](https://mantine.dev/llms/core-multi-select.md): Custom searchable multi select
- [NativeSelect](https://mantine.dev/llms/core-native-select.md): Native select element based on Input
- [NavLink](https://mantine.dev/llms/core-nav-link.md): Navigation link
- [Notification](https://mantine.dev/llms/core-notification.md): Show dynamic notifications and alerts to user, part of notifications system
- [NumberFormatter](https://mantine.dev/llms/core-number-formatter.md): Format number with thousands/decimal separators and suffix/prefix
- [NumberInput](https://mantine.dev/llms/core-number-input.md): Capture number from user
- [OverflowList](https://mantine.dev/llms/core-overflow-list.md): Hide items that do not fit in a container and display them as a single collapsed item
- [Overlay](https://mantine.dev/llms/core-overlay.md): Overlays parent element with div element with any color and opacity
- [Pagination](https://mantine.dev/llms/core-pagination.md): Display active page and navigate between multiple pages
- [Paper](https://mantine.dev/llms/core-paper.md): Renders white or dark background depending on color scheme
- [PasswordInput](https://mantine.dev/llms/core-password-input.md): Capture password data from user
- [Pill](https://mantine.dev/llms/core-pill.md): Removable and non-removable tags
- [PillsInput](https://mantine.dev/llms/core-pills-input.md): Base component for custom tags inputs and multi selects
- [PinInput](https://mantine.dev/llms/core-pin-input.md): Capture pin code or one time password from the user
- [Popover](https://mantine.dev/llms/core-popover.md): Display popover section relative to given target element
- [Portal](https://mantine.dev/llms/core-portal.md): Renders component outside of parent element tree
- [Progress](https://mantine.dev/llms/core-progress.md): Give user feedback for status of the task
- [Radio](https://mantine.dev/llms/core-radio.md): Wrapper for input type radio
- [RangeSlider](https://mantine.dev/llms/core-range-slider.md): RangeSlider component
- [Rating](https://mantine.dev/llms/core-rating.md): Pick and display rating
- [RingProgress](https://mantine.dev/llms/core-ring-progress.md): Give user feedback for status of the task with circle diagram
- [RollingNumber](https://mantine.dev/llms/core-rolling-number.md): Animate value changes with rolling digits
- [ScrollArea](https://mantine.dev/llms/core-scroll-area.md): Area with custom scrollbars
- [Scroller](https://mantine.dev/llms/core-scroller.md): Horizontal scroll container with navigation controls
- [SegmentedControl](https://mantine.dev/llms/core-segmented-control.md): A linear set of two or more segments
- [Select](https://mantine.dev/llms/core-select.md): Custom searchable select
- [SemiCircleProgress](https://mantine.dev/llms/core-semi-circle-progress.md): Represent progress with semi circle diagram
- [SimpleGrid](https://mantine.dev/llms/core-simple-grid.md): Responsive grid in which each item takes equal amount of space
- [Skeleton](https://mantine.dev/llms/core-skeleton.md): Indicate content loading state
- [Slider](https://mantine.dev/llms/core-slider.md): Slider component
- [Space](https://mantine.dev/llms/core-space.md): Add horizontal or vertical spacing from theme
- [Splitter](https://mantine.dev/llms/core-splitter.md): Resizable split pane layout
- [Spoiler](https://mantine.dev/llms/core-spoiler.md): Hide long sections of content under a spoiler
- [Stack](https://mantine.dev/llms/core-stack.md): Compose elements and components in a vertical flex container
- [Stepper](https://mantine.dev/llms/core-stepper.md): Display content divided into a steps sequence
- [Switch](https://mantine.dev/llms/core-switch.md): Capture boolean input from user
- [Table](https://mantine.dev/llms/core-table.md): Render table with theme styles
- [TableOfContents](https://mantine.dev/llms/core-table-of-contents.md): Renders a list of headings on the page and tracks current heading visible in the viewport
- [Tabs](https://mantine.dev/llms/core-tabs.md): Switch between different views
- [TagsInput](https://mantine.dev/llms/core-tags-input.md): Capture a list of values from user with free input and suggestions
- [Text](https://mantine.dev/llms/core-text.md): Display text
- [Textarea](https://mantine.dev/llms/core-textarea.md): Autosize or regular textarea
- [TextInput](https://mantine.dev/llms/core-text-input.md): Capture string input from user
- [ThemeIcon](https://mantine.dev/llms/core-theme-icon.md): Render icon inside element with theme colors
- [Timeline](https://mantine.dev/llms/core-timeline.md): Display list of events in chronological order
- [Title](https://mantine.dev/llms/core-title.md): h1-h6 heading
- [Tooltip](https://mantine.dev/llms/core-tooltip.md): Renders tooltip at given element on mouse over or other event
- [Transition](https://mantine.dev/llms/core-transition.md): Animate presence of component with pre-made animations
- [Tree](https://mantine.dev/llms/core-tree.md): Display a Tree structure
- [TreeSelect](https://mantine.dev/llms/core-tree-select.md): Select with hierarchical tree data
- [Typography](https://mantine.dev/llms/core-typography.md): Styles provider for html content
- [UnstyledButton](https://mantine.dev/llms/core-unstyled-button.md): Unstyled polymorphic button
- [VisuallyHidden](https://mantine.dev/llms/core-visually-hidden.md): Hide element visually but keep it accessible for screen readers
## Hooks
- [HooksPackage](https://mantine.dev/llms/hooks-package.md)
- [use-element-size](https://mantine.dev/llms/hooks-use-element-size.md): Returns element width and height and observes changes with ResizeObserver
- [useClickOutside](https://mantine.dev/llms/hooks-use-click-outside.md)
- [useClipboard](https://mantine.dev/llms/hooks-use-clipboard.md)
- [useCollapse](https://mantine.dev/llms/hooks-use-collapse.md)
- [useColorScheme](https://mantine.dev/llms/hooks-use-color-scheme.md)
- [useCounter](https://mantine.dev/llms/hooks-use-counter.md)
- [useDebouncedCallback](https://mantine.dev/llms/hooks-use-debounced-callback.md)
- [useDebouncedState](https://mantine.dev/llms/hooks-use-debounced-state.md)
- [useDebouncedValue](https://mantine.dev/llms/hooks-use-debounced-value.md)
- [useDidUpdate](https://mantine.dev/llms/hooks-use-did-update.md)
- [useDisclosure](https://mantine.dev/llms/hooks-use-disclosure.md)
- [useDocumentTitle](https://mantine.dev/llms/hooks-use-document-title.md)
- [useDocumentVisibility](https://mantine.dev/llms/hooks-use-document-visibility.md)
- [useDrag](https://mantine.dev/llms/hooks-use-drag.md)
- [useEventListener](https://mantine.dev/llms/hooks-use-event-listener.md)
- [useEyeDropper](https://mantine.dev/llms/hooks-use-eye-dropper.md)
- [useFavicon](https://mantine.dev/llms/hooks-use-favicon.md)
- [useFetch](https://mantine.dev/llms/hooks-use-fetch.md)
- [useFileDialog](https://mantine.dev/llms/hooks-use-file-dialog.md)
- [useFloatingWindow](https://mantine.dev/llms/hooks-use-floating-window.md)
- [useFocusReturn](https://mantine.dev/llms/hooks-use-focus-return.md)
- [useFocusTrap](https://mantine.dev/llms/hooks-use-focus-trap.md)
- [useFocusWithin](https://mantine.dev/llms/hooks-use-focus-within.md)
- [useForceUpdate](https://mantine.dev/llms/hooks-use-force-update.md)
- [useFullscreen](https://mantine.dev/llms/hooks-use-fullscreen.md)
- [useHash](https://mantine.dev/llms/hooks-use-hash.md)
- [useHeadroom](https://mantine.dev/llms/hooks-use-headroom.md)
- [useHotkeys](https://mantine.dev/llms/hooks-use-hotkeys.md)
- [useHover](https://mantine.dev/llms/hooks-use-hover.md)
- [useId](https://mantine.dev/llms/hooks-use-id.md)
- [useIdle](https://mantine.dev/llms/hooks-use-idle.md)
- [useInputState](https://mantine.dev/llms/hooks-use-input-state.md)
- [useIntersection](https://mantine.dev/llms/hooks-use-intersection.md)
- [useInterval](https://mantine.dev/llms/hooks-use-interval.md)
- [useInViewport](https://mantine.dev/llms/hooks-use-in-viewport.md)
- [useIsFirstRender](https://mantine.dev/llms/hooks-use-is-first-render.md)
- [useIsomorphicEffect](https://mantine.dev/llms/hooks-use-isomorphic-effect.md)
- [useListState](https://mantine.dev/llms/hooks-use-list-state.md)
- [useLocalStorage](https://mantine.dev/llms/hooks-use-local-storage.md)
- [useLogger](https://mantine.dev/llms/hooks-use-logger.md)
- [useLongPress](https://mantine.dev/llms/hooks-use-long-press.md)
- [useMap](https://mantine.dev/llms/hooks-use-map.md)
- [useMask](https://mantine.dev/llms/hooks-use-mask.md)
- [useMediaQuery](https://mantine.dev/llms/hooks-use-media-query.md)
- [useMergedRef](https://mantine.dev/llms/hooks-use-merged-ref.md)
- [useMounted](https://mantine.dev/llms/hooks-use-mounted.md)
- [useMouse](https://mantine.dev/llms/hooks-use-mouse.md)
- [useMove](https://mantine.dev/llms/hooks-use-move.md)
- [useMutationObserver](https://mantine.dev/llms/hooks-use-mutation-observer.md)
- [useNetwork](https://mantine.dev/llms/hooks-use-network.md)
- [useOrientation](https://mantine.dev/llms/hooks-use-orientation.md)
- [useOs](https://mantine.dev/llms/hooks-use-os.md)
- [usePageLeave](https://mantine.dev/llms/hooks-use-page-leave.md)
- [usePagination](https://mantine.dev/llms/hooks-use-pagination.md)
- [usePrevious](https://mantine.dev/llms/hooks-use-previous.md)
- [useQueue](https://mantine.dev/llms/hooks-use-queue.md)
- [useRadialMove](https://mantine.dev/llms/hooks-use-radial-move.md)
- [useReducedMotion](https://mantine.dev/llms/hooks-use-reduced-motion.md)
- [useResizeObserver](https://mantine.dev/llms/hooks-use-resize-observer.md)
- [useRovingIndex](https://mantine.dev/llms/hooks-use-roving-index.md)
- [useScrollDirection](https://mantine.dev/llms/hooks-use-scroll-direction.md)
- [useScroller](https://mantine.dev/llms/hooks-use-scroller.md)
- [useScrollIntoView](https://mantine.dev/llms/hooks-use-scroll-into-view.md)
- [useScrollSpy](https://mantine.dev/llms/hooks-use-scroll-spy.md)
- [useSelection](https://mantine.dev/llms/hooks-use-selection.md)
- [useSet](https://mantine.dev/llms/hooks-use-set.md)
- [useSetState](https://mantine.dev/llms/hooks-use-set-state.md)
- [useShallowEffect](https://mantine.dev/llms/hooks-use-shallow-effect.md)
- [useSplitter](https://mantine.dev/llms/hooks-use-splitter.md)
- [useStateHistory](https://mantine.dev/llms/hooks-use-state-history.md)
- [useTextSelection](https://mantine.dev/llms/hooks-use-text-selection.md)
- [useThrottledCallback](https://mantine.dev/llms/hooks-use-throttled-callback.md)
- [useThrottledState](https://mantine.dev/llms/hooks-use-throttled-state.md)
- [useThrottledValue](https://mantine.dev/llms/hooks-use-throttled-value.md)
- [useTimeout](https://mantine.dev/llms/hooks-use-timeout.md)
- [useToggle](https://mantine.dev/llms/hooks-use-toggle.md)
- [useUncontrolled](https://mantine.dev/llms/hooks-use-uncontrolled.md)
- [useValidatedState](https://mantine.dev/llms/hooks-use-validated-state.md)
- [useViewportSize](https://mantine.dev/llms/hooks-use-viewport-size.md)
- [useWindowEvent](https://mantine.dev/llms/hooks-use-window-event.md)
- [useWindowScroll](https://mantine.dev/llms/hooks-use-window-scroll.md)
## Form
- [Form context](https://mantine.dev/llms/form-create-form-context.md): Add context support to use-form with createFormContext
- [formActions](https://mantine.dev/llms/form-actions.md)
- [formAllInputs](https://mantine.dev/llms/form-all-inputs.md)
- [formErrors](https://mantine.dev/llms/form-errors.md)
- [formGetInputProps](https://mantine.dev/llms/form-get-input-props.md)
- [formNested](https://mantine.dev/llms/form-nested.md)
- [FormPackage](https://mantine.dev/llms/form-package.md)
- [formRecipes](https://mantine.dev/llms/form-recipes.md)
- [formSchemaValidation](https://mantine.dev/llms/form-schema-validation.md)
- [formStatus](https://mantine.dev/llms/form-status.md)
- [formUncontrolled](https://mantine.dev/llms/form-uncontrolled.md)
- [formValidation](https://mantine.dev/llms/form-validation.md)
- [formValidators](https://mantine.dev/llms/form-validators.md)
- [formValues](https://mantine.dev/llms/form-values.md)
- [use-field](https://mantine.dev/llms/form-use-field.md): use-field hook – manage single field state
- [use-form](https://mantine.dev/llms/form-use-form.md): Manage form state
## Dates
- [Calendar](https://mantine.dev/llms/dates-calendar.md): Base component for custom date pickers
- [DateInput](https://mantine.dev/llms/dates-date-input.md): Free form date input
- [DatePicker](https://mantine.dev/llms/dates-date-picker.md): Inline date, multiple dates and dates range picker
- [DatePickerInput](https://mantine.dev/llms/dates-date-picker-input.md): Date, multiple dates and dates range picker input
- [DateTimePicker](https://mantine.dev/llms/dates-date-time-picker.md): Capture datetime from the user
- [GettingStartedDates](https://mantine.dev/llms/dates-getting-started.md)
- [InlineDateTimePicker](https://mantine.dev/llms/dates-inline-date-time-picker.md): Inline date and time picker with range support
- [MiniCalendar](https://mantine.dev/llms/dates-mini-calendar.md): Compact calendar to display a small number of days in a row
- [MonthPicker](https://mantine.dev/llms/dates-month-picker.md): Inline month, multiple months and months range picker
- [MonthPickerInput](https://mantine.dev/llms/dates-month-picker-input.md): Month, multiple months and months range picker input
- [TimeGrid](https://mantine.dev/llms/dates-time-grid.md): Captures time value from the user with a predefined set of options
- [TimeInput](https://mantine.dev/llms/dates-time-input.md): Capture time from the user
- [TimePicker](https://mantine.dev/llms/dates-time-picker.md): Captures time value from the user
- [TimeValue](https://mantine.dev/llms/dates-time-value.md): Display a formatted time value
- [YearPicker](https://mantine.dev/llms/dates-year-picker.md): Inline year, multiple years and years range picker
- [YearPickerInput](https://mantine.dev/llms/dates-year-picker-input.md): Inline year, multiple years and years range picker
## Charts
- [AreaChart](https://mantine.dev/llms/charts-area-chart.md): Area chart component with stacked, percent and split variants
- [BarChart](https://mantine.dev/llms/charts-bar-chart.md): Bar chart component with stacked and percent variants
- [BarsList](https://mantine.dev/llms/charts-bars-list.md): Display a list of bars with names and values
- [BubbleChart](https://mantine.dev/llms/charts-bubble-chart.md): Bubble chart component
- [BulletChart](https://mantine.dev/llms/charts-bullet-chart.md): Compact KPI chart with value, target, and qualitative ranges
- [CompositeChart](https://mantine.dev/llms/charts-composite-chart.md): Composed chart with support for Area, Bar and Line charts
- [DonutChart](https://mantine.dev/llms/charts-donut-chart.md): Donut chart component
- [FunnelChart](https://mantine.dev/llms/charts-funnel-chart.md): Funnel chart component
- [GettingStartedCharts](https://mantine.dev/llms/charts-getting-started.md)
- [Heatmap](https://mantine.dev/llms/charts-heatmap.md): Heatmap chart component
- [LineChart](https://mantine.dev/llms/charts-line-chart.md): Line chart component
- [PieChart](https://mantine.dev/llms/charts-pie-chart.md): Pie chart component
- [RadarChart](https://mantine.dev/llms/charts-radar-chart.md): Radar chart component
- [RadialBarChart](https://mantine.dev/llms/charts-radial-bar-chart.md): Radial bar chart component
- [SankeyChart](https://mantine.dev/llms/charts-sankey-chart.md): Sankey diagram component
- [ScatterChart](https://mantine.dev/llms/charts-scatter-chart.md): Scatter chart component
- [Sparkline](https://mantine.dev/llms/charts-sparkline.md): Simplified area chart to show trends
- [SunburstChart](https://mantine.dev/llms/charts-sunburst-chart.md): Sunburst chart component for hierarchical data
- [Treemap](https://mantine.dev/llms/charts-treemap.md): Treemap chart component
## Schedule
- [AgendaView](https://mantine.dev/llms/schedule-agenda-view.md): Vertical list view of events grouped by date
- [DayView](https://mantine.dev/llms/schedule-day-view.md): Standalone schedule day view component
- [GettingStartedSchedule](https://mantine.dev/llms/schedule-getting-started.md)
- [MobileMonthView](https://mantine.dev/llms/schedule-mobile-month-view.md): Mobile-optimized schedule month view component
- [MonthView](https://mantine.dev/llms/schedule-month-view.md): Standalone schedule month view component
- [RecurringEventsSchedule](https://mantine.dev/llms/schedule-recurring-events.md)
- [ResourcesDayView](https://mantine.dev/llms/schedule-resources-day-view.md): Day view with resource columns for scheduling across resources
- [ResourcesMonthView](https://mantine.dev/llms/schedule-resources-month-view.md): Month view with resource lanes for scheduling across resources
- [ResourcesSchedule](https://mantine.dev/llms/schedule-resources-schedule.md): Schedule wrapper component that combines resource day, week and month views
- [ResourcesWeekView](https://mantine.dev/llms/schedule-resources-week-view.md): Week view with resource rows for scheduling across resources
- [Schedule](https://mantine.dev/llms/schedule-schedule.md): Schedule component with day, week, month and year views
- [ScheduleEventsData](https://mantine.dev/llms/schedule-events-data.md)
- [WeekView](https://mantine.dev/llms/schedule-week-view.md): Standalone schedule week view component
- [YearView](https://mantine.dev/llms/schedule-year-view.md): Standalone schedule year view component
## Guides
- [ControlledVsUncontrolled](https://mantine.dev/llms/guides-controlled-vs-uncontrolled.md)
- [CustomComponents](https://mantine.dev/llms/guides-custom-components.md)
- [EightToNine](https://mantine.dev/llms/guides-8x-to-9x.md)
- [FunctionsReference](https://mantine.dev/llms/guides-functions-reference.md)
- [Gatsby](https://mantine.dev/llms/guides-gatsby.md)
- [Icons](https://mantine.dev/llms/guides-icons.md)
- [JavaScript](https://mantine.dev/llms/guides-javascript.md)
- [Jest](https://mantine.dev/llms/guides-jest.md)
- [LLMDocumentation](https://mantine.dev/llms/guides-llms.md)
- [NextJs](https://mantine.dev/llms/guides-next.md)
- [Polymorphic](https://mantine.dev/llms/guides-polymorphic.md)
- [ReactRouter](https://mantine.dev/llms/guides-react-router.md)
- [Redwood](https://mantine.dev/llms/guides-redwood.md)
- [SevenToEight](https://mantine.dev/llms/guides-7x-to-8x.md)
- [SixToSeven](https://mantine.dev/llms/guides-6x-to-7x.md)
- [Storybook](https://mantine.dev/llms/guides-storybook.md)
- [Tiptap3Migration](https://mantine.dev/llms/guides-tiptap-3-migration.md)
- [TypeScript](https://mantine.dev/llms/guides-typescript.md)
- [Vite](https://mantine.dev/llms/guides-vite.md)
- [Vitest](https://mantine.dev/llms/guides-vitest.md)
## Theming
- [Colors](https://mantine.dev/llms/theming-colors.md)
- [ColorSchemes](https://mantine.dev/llms/theming-color-schemes.md)
- [DefaultProps](https://mantine.dev/llms/theming-default-props.md)
- [MantineProvider](https://mantine.dev/llms/theming-mantine-provider.md)
- [ThemeObject](https://mantine.dev/llms/theming-theme-object.md)
- [Typography](https://mantine.dev/llms/theming-typography.md): Styles provider for html content
## Styles
- [ColorFunctions](https://mantine.dev/llms/styles-color-functions.md)
- [CSSFilesList](https://mantine.dev/llms/styles-css-files-list.md)
- [CSSModules](https://mantine.dev/llms/styles-css-modules.md)
- [CssVariables](https://mantine.dev/llms/styles-css-variables.md)
- [CSSVariablesList](https://mantine.dev/llms/styles-css-variables-list.md)
- [DataAttributes](https://mantine.dev/llms/styles-data-attributes.md)
- [Emotion](https://mantine.dev/llms/styles-emotion.md)
- [GlobalStyles](https://mantine.dev/llms/styles-global-styles.md)
- [MantineStyles](https://mantine.dev/llms/styles-mantine-styles.md)
- [PostCSSPreset](https://mantine.dev/llms/styles-postcss-preset.md)
- [Rem](https://mantine.dev/llms/styles-rem.md)
- [ResponsiveStyles](https://mantine.dev/llms/styles-responsive.md)
- [Rtl](https://mantine.dev/llms/styles-rtl.md)
- [StyleProp](https://mantine.dev/llms/styles-style.md)
- [StyleProps](https://mantine.dev/llms/styles-style-props.md)
- [StylesApi](https://mantine.dev/llms/styles-styles-api.md)
- [StylesOverview](https://mantine.dev/llms/styles-styles-overview.md)
- [StylesPerformance](https://mantine.dev/llms/styles-styles-performance.md)
- [UnstyledComponents](https://mantine.dev/llms/styles-unstyled.md)
- [UsageWithSass](https://mantine.dev/llms/styles-sass.md)
- [VanillaExtract](https://mantine.dev/llms/styles-vanilla-extract.md)
- [VariantsAndSizes](https://mantine.dev/llms/styles-variants-sizes.md)
## X
- [Carousel](https://mantine.dev/llms/x-carousel.md): Embla based carousel component
- [CodeHighlight](https://mantine.dev/llms/x-code-highlight.md): Highlight code with shiki or highlight.js
- [Dropzone](https://mantine.dev/llms/x-dropzone.md): Capture files from user with drag and drop
- [ExtensionsPackage](https://mantine.dev/llms/x-extensions.md)
- [Modals manager](https://mantine.dev/llms/x-modals.md): Centralized modals manager with option to handle state of multi-step modals
- [NavigationProgress](https://mantine.dev/llms/x-nprogress.md): Navigation progress bar
- [Notifications system](https://mantine.dev/llms/x-notifications.md): Mantine notifications system
- [Rich text editor](https://mantine.dev/llms/x-tiptap.md): Tiptap based rich text editor
- [Spotlight](https://mantine.dev/llms/x-spotlight.md): Command center for your application
## Other
- [About](https://mantine.dev/llms/about.md)
- [AllReleases](https://mantine.dev/llms/changelog-all-releases.md)
- [BrowserSupport](https://mantine.dev/llms/browser-support.md)
- [Changelog700](https://mantine.dev/llms/changelog-7-0-0.md)
- [Changelog710](https://mantine.dev/llms/changelog-7-1-0.md)
- [Changelog7100](https://mantine.dev/llms/changelog-7-10-0.md)
- [Changelog7110](https://mantine.dev/llms/changelog-7-11-0.md)
- [Changelog7120](https://mantine.dev/llms/changelog-7-12-0.md)
- [Changelog7130](https://mantine.dev/llms/changelog-7-13-0.md)
- [Changelog7140](https://mantine.dev/llms/changelog-7-14-0.md)
- [Changelog7150](https://mantine.dev/llms/changelog-7-15-0.md)
- [Changelog7160](https://mantine.dev/llms/changelog-7-16-0.md)
- [Changelog7170](https://mantine.dev/llms/changelog-7-17-0.md)
- [Changelog720](https://mantine.dev/llms/changelog-7-2-0.md)
- [Changelog730](https://mantine.dev/llms/changelog-7-3-0.md)
- [Changelog740](https://mantine.dev/llms/changelog-7-4-0.md)
- [Changelog750](https://mantine.dev/llms/changelog-7-5-0.md)
- [Changelog760](https://mantine.dev/llms/changelog-7-6-0.md)
- [Changelog770](https://mantine.dev/llms/changelog-7-7-0.md)
- [Changelog780](https://mantine.dev/llms/changelog-7-8-0.md)
- [Changelog790](https://mantine.dev/llms/changelog-7-9-0.md)
- [Changelog800](https://mantine.dev/llms/changelog-8-0-0.md)
- [Changelog810](https://mantine.dev/llms/changelog-8-1-0.md)
- [Changelog820](https://mantine.dev/llms/changelog-8-2-0.md)
- [Changelog830](https://mantine.dev/llms/changelog-8-3-0.md)
- [Changelog900](https://mantine.dev/llms/changelog-9-0-0.md)
- [Changelog910](https://mantine.dev/llms/changelog-9-1-0.md)
- [Changelog920](https://mantine.dev/llms/changelog-9-2-0.md)
- [Changelog930](https://mantine.dev/llms/changelog-9-3-0.md)
- [Changelog940](https://mantine.dev/llms/changelog-9-4-0.md)
- [Changelog950](https://mantine.dev/llms/changelog-9-5-0.md)
- [Contribute](https://mantine.dev/llms/contribute.md)
- [GettingStarted](https://mantine.dev/llms/getting-started.md)
- [NotFound](https://mantine.dev/llms/404.md)
- [OxcConfig](https://mantine.dev/llms/oxc-config-mantine.md)
- [PreviousChangelogs](https://mantine.dev/llms/changelog-previous-versions.md)
- [Support](https://mantine.dev/llms/support.md)
+10 -16
View File
@@ -6,34 +6,28 @@ product
## Users ## Users
Homarr is for self-hosters and administrators who assemble dashboards for services across home labs and private infrastructure. They need dense, reliable controls and status at a glance, while retaining enough guidance to configure advanced features without hiding how they work. Homarr is used by self-hosters and administrators who assemble dashboards for services running across home labs and private infrastructure. They need dense, reliable controls and status at a glance, with enough guidance to configure advanced features without hiding how they work.
## Product Purpose ## Product Purpose
Homarr provides one place to organize, observe, and operate self-hosted services. Success means users can build useful dashboards quickly, understand their state, recover from configuration errors, act on services, and reshape their workspace without repeatedly leaving the board. Boards must load immediately, stay visually stable, and keep widgets legible across screen sizes. Homarr provides one place to organize, observe, and operate self-hosted services. Success means a user can build a useful dashboard quickly, understand its state, recover from configuration errors, and extend it without requiring a bespoke Homarr feature for every service.
## Brand Personality ## Brand Personality
Direct, capable, precise, and approachable. Homarr should feel calm under load, be truthful about system state, and reward users who refine details. Direct, capable, and approachable. Copy should be concise and technical when precision matters, while the interface should remain familiar to users who are not developers.
## Anti-references ## Anti-references
- Decorative dashboards, oversized metric cards, and motion without state meaning. Avoid decorative admin dashboards, wizard flows that fight ordinary scrolling, hidden configuration magic, inconsistent controls, excessive modal usage, and interfaces that expose implementation complexity before the user needs it.
- Wizard flows that fight ordinary scrolling or rigid layouts that force one workflow.
- Hidden configuration magic and implementation complexity exposed before it is needed.
- Clever custom controls where familiar platform or Mantine interactions already work.
## Design Principles ## Design Principles
1. Keep configuration, consequences, and system state visible. 1. Keep the user in control by making configuration and consequences visible.
2. Make simple tasks short without limiting advanced workflows. 2. Prefer familiar product patterns and progressive disclosure over custom interaction concepts.
3. Preserve expert depth while prioritizing urgent information at small sizes. 3. Make simple tasks short without limiting advanced use cases.
4. Reveal dense workspaces progressively through a consistent advanced focus mode. 4. Isolate failures so one service or widget cannot disrupt the dashboard.
5. Isolate failures and show loading, stale, partial, empty, and failed states truthfully. 5. Use the existing Mantine design language consistently across management and board surfaces.
6. Use the existing Mantine design language consistently across management and board surfaces.
7. Keep spatial actions direct: moving, resizing, nesting, and using fixed rails happen on the canvas with immediate feedback.
8. Preserve predictable widget composition with fixed logical tile sizes and horizontal scrolling before controls become inaccessible.
## Accessibility & Inclusion ## Accessibility & Inclusion
Target WCAG 2.2 AA. Every pointer interaction needs a keyboard and coarse-pointer path. Support screen readers, visible and restored focus, sufficient contrast, reduced motion, non-color status cues, responsive layouts, actionable error messages, and keyboard equivalents for drag and resize operations. Target WCAG 2.2 AA. Support keyboard navigation, screen readers, visible focus, sufficient contrast, reduced motion, non-color status cues, responsive layouts, and actionable error messages.
-263
View File
@@ -1,263 +0,0 @@
# PR #6502 widget-modernization ultra-review
## Executive result
**Merge recommendation: changes required.** No P0 was found, but the current branch still has five P1 issues: a pre-existing Custom API authorization/redirect weakness exposed by this subsystem review, an inaccessible manual advanced-focus interaction, a likely fixed-position/transform geometry defect, systemic false empty states for failed queries, and unvalidated integration-provided navigation URLs. Eight P2 issues cover raw error disclosure, unsafe runtime-ref interfaces, localization, responsive/theming drift, test coverage, and inaccurate PR copy.
Audit health (Impeccable rubric): accessibility 1/4, performance 2/4, responsive design 2/4, theming 2/4, anti-patterns 3/4; **10/20 (Acceptable, significant work required)**. The UI does not look generically AI-generated; its problem is inconsistent technical behavior beneath an otherwise coherent Mantine vocabulary.
## Reviewed revision and environment
| Item | Value |
| ---------------------------- | ---------------------------------------------------------------------------------------------- |
| PR | #6502, `feat: modernize dashboard widgets` |
| Reviewed head | `96e92485a5fd45f3648cfac0453e614779c414ba` |
| Base branch / fetched base | `dev` / `d58a47b776a55d04dee2220f59d072b9af8bbed3` |
| Merge base | `d58a47b776a55d04dee2220f59d072b9af8bbed3` |
| Diff | 359 files, +15,721/-4,478 |
| PR state | open, non-draft, `BLOCKED`; no approval decision |
| Hosted checks at audit start | Fast gate, Container and E2E, CodeQL, JS/TS analysis, Actions analysis, title, auto-fix: green |
| Inline threads | 34 total, 7 unresolved; only 2 remain substantively valid |
| Mantine | repository lock: `@mantine/core`, hooks, charts and dates 9.4.1 |
| Local tools | Node 24.16.0 and pnpm 9.5.0; repository requires Node >=24.18.0 and pnpm >=11.15.1 |
The former red baseline `7b7045202d289b7d83946befcee5d2845ea4a706` had a green Fast gate and a failed Container/E2E run (`30748255760`, job `91498308523`). The latest head has a green Container/E2E job (`30756067198`, job `91519039504`). This proves the current suite passed once; it does not satisfy the requested three-run flake characterization.
## PR-description traceability
| Claim | Result | Evidence / correction |
| --------------------------------------------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| “all 54 widget renderers with compact and advanced modes” | **Inaccurate** | 54 `WidgetKind` values exist. `displayMode="advanced"` reaches 53; `app` is deliberately excluded in `item-content.tsx:51` and documented as compact-only. Change copy to “53 widget renderers; app tiles remain compact,” or implement app advanced mode. |
| advanced focus: hover, keyboard, touch, geometry | **Partial** | Entry paths, Escape ownership, reduced motion, close focus, touch button and zero-dimension guards exist. WID-002/003 remain. |
| richer widget context actions | **Partial** | `Menu.ContextMenu` is a good Mantine choice and query scope is item-aware. Dispatch still crosses `any`; Custom API action authorization is not represented. |
| standardized query identity/cache/status | **Partial** | Query keys, matchers, runtime capture, recovery and tests exist. Thirty-nine of 53 renderer query calls do not read errors; runtime ref registration is render-time mutable state. |
| safe polling | **Mostly confirmed** | defaults are limited to `widget` namespace; Custom API resume immediately refetches; per-widget intervals exist. State/ref typing still needs redesign. |
| partial-failure rendering | **Partial** | settlement helper and several routers are strong; many renderer-level total failures still appear empty/disconnected. |
| bounded integration responses | **Mostly confirmed** | common bounded reader, Custom API 2 MiB, timetable 512 KiB, Traefik bounds and Beszel queue bounds exist. Custom action execution remains outside the hardened fetch path. |
| timetable hardening | **Confirmed** | board/item permission checks, saved-value comparison, public-address resolution, private ranges, per-request DNS re-resolution, pinned dispatcher, redirect rejection, timeouts, response bounds and schemas are present. |
| precise placement | **Mostly confirmed** | mouse, touch tolerance, keyboard, occupied-cell validation, dynamic bounds, nearest fit and immediate persistence exist. Failure/race/live responsive coverage remains insufficient. |
| widget catalog documentation | **Partial** | all kinds have pages and shared interactions; wording overclaims focus behavior and validation counts are stale. |
| validation: 40 files/162 tests, 6/6 E2E | **Stale** | current diff contains 57 changed unit-spec files (including three E2E specs); repository has 42 widget spec files and six E2E specs. Update from the exact final run. |
| independent reviews “pass” | **Not supported** | unresolved valid findings, current P1s, and unavailable live QA contradict an unqualified pass. |
## Mantine suitability ledger
Legend: **U** already used appropriately; **C** consolidation/replacement candidate; **R** relevant but current implementation can stay; **N** not relevant to widget runtime; **D** dependency/version addition. Exact 9.4.1 package archives were inspected in `/tmp`; live docs/Context7 include 9.5.0.
### Core catalog
- **U:** ActionIcon, Alert, Anchor, Avatar, Badge, Box, Button, Card, Center, Code, Divider, Flex, Grid, Group, Image, Loader, Menu (`Menu.ContextMenu`), Overlay, Paper, Popover, Portal, Progress, RingProgress, ScrollArea, SimpleGrid, Skeleton, Stack, Table, Tabs, Text, ThemeIcon, Title, Tooltip, UnstyledButton.
- **C:** Accordion (dense integration sections), DataList (semantic label/value stats), EmptyState (replace bespoke empty state), FocusTrap and Modal compound API (WID-002), LoadingOverlay (background refetch without deleting content), NumberFormatter, OverflowList/Scroller (responsive action and chip rows), Table.ScrollContainer, Timeline (activity/feed histories), VisuallyHidden and Kbd, CloseButton, Dialog/HoverCard/Notification where persistent errors/actions need semantics.
- **R:** Affix, AppShell, AspectRatio, BackgroundImage, Breadcrumbs, Burger, Checkbox, Chip, Collapse, ColorInput/ColorPicker/ColorSwatch/AlphaSlider/AngleSlider/HueSlider, Combobox/ComboboxPopover, Container, CopyButton, Drawer, Fieldset, FileButton/FileInput, FloatingIndicator/FloatingWindow, Highlight, Indicator, Input/JsonInput, List, Mark, Marquee, MaskInput, Menubar, MultiSelect/NativeSelect/NumberInput/PasswordInput/PillsInput/PinInput/Radio/RangeSlider/Rating/SegmentedControl/Select/Slider/Switch/TagsInput/Textarea/TextInput, Pagination, Pill, SemiCircleProgress, Space, Splitter, Spoiler, Stepper, Transition, Tree/TreeSelect, Typography. Use only where an existing widget requirement matches; do not churn working layouts.
- **N:** Alpha/color/angle primitives outside configuration forms, AppShell/navigation primitives, TableOfContents, CorePackage documentation.
- **D (9.5, not 9.4.1):** Cascader. **RollingNumber and Splitter are present in 9.4.1.**
### Hooks catalog
- **C:** `useFocusReturn`, `useFocusTrap`, `useReducedMotion`, `useRovingIndex`, `useLongPress`, `useScroller`, `useElementSize`, `useViewportSize`, `useHotkeys`, `useMediaQuery`, `useResizeObserver`.
- **U/R:** focus-within, hover, disclosure, click-outside, merged-ref, interval/timeout, local-storage, event/window listeners, throttled/debounced values and list/set/map/state helpers are available in 9.4.1 and should replace local mechanics only when behavior and tests improve.
- **N:** document title/favicon/hash/headroom/page-leave/OS/eye-dropper/fullscreen/network/orientation and other page-level utilities have no direct widget-remediation need. Every hook listed under `## Hooks` in `MANTINE.md` is covered by this rule; no hook requires 9.5 for the recommendations above.
### Form, dates, charts, schedule, extensions, theme and styles
- **Form:** existing `@homarr/form` should remain the abstraction over all cataloged form APIs. No widget-runtime replacement; use current validation/status APIs for widget editors (**R**).
- **Dates:** all catalog entries are in installed 9.4.1 dates. `Calendar`, `MiniCalendar`, and `TimeValue` are candidates; other picker/input entries are editor-only (**R**). Importing dates directly into `@homarr/widgets` requires adding that workspace dependency (**D**).
- **Charts:** 9.4.1 exports Area, Bar, BarsList, Bubble, Composite, Donut, Funnel, Heatmap, Line, Pie, Radar, RadialBar, Sankey, Scatter, Sparkline and Treemap (**C/R**). Prefer Sparkline for tiny trends, BarsList for ranked values, Heatmap for calendar/activity density, Composite for mixed analytics. **BulletChart and SunburstChart are 9.5-only (D)**. Replacement requires accessibility and bundle comparison, not blanket Recharts removal.
- **Schedule:** all `AgendaView`, day/week/month/year/resource views and Schedule pages require new `@mantine/schedule` dependency (**D**). AgendaView is the only plausible calendar-widget candidate; its weight and semantics must beat the existing grouped list before adoption.
- **Guides/FAQ/Other:** server components, portal testing, scroll lock, accessibility, nested popovers, zoom, responsive styling and version changelogs are relevant references (**R**); framework/setup/contribution/old migration entries are **N**. The 9.5 changelog is informational only.
- **Theming/styles:** Colors, ColorSchemes, DefaultProps, MantineProvider, ThemeObject, CSS variables, data attributes, CSS Modules, responsive/RTL/style props/Styles API/performance are **U/R**. WID-009 requires shared layer/color tokens. Emotion, Sass, Vanilla Extract and unrelated global setup are **N**.
- **Extensions:** existing modals, notifications, spotlight and rich-text abstractions are **U**; Carousel could consolidate Immich only after behavior parity; Dropzone/CodeHighlight/NProgress are **N** for this audit.
## Complete 54-widget coverage matrix
“Test” means a focused widget-local spec exists, not that all required states are covered. Every row still requires the common contract suite in WID-013.
| WidgetKind | Family | Advanced | Focused test | Principal audit gap |
| --------------------------- | ----------------------- | ----------- | ------------ | ---------------------------------------------------------- |
| clock | basic | yes | no | query error, locale-format options |
| weather | basic/feed | yes | yes | query error; dayjs locale/time patterns |
| app | basic/link | **no** | no | PR wording; URL contract |
| iframe | custom content | yes | yes | manual focus/iframe trapping; URL privacy thread now fixed |
| video | custom content | yes | no | media/focus/live behavior |
| notebook | notes | yes (inner) | yes | editor focus inside manual surface |
| anchorNote | notes | yes | no | query error, save/error regression |
| dnsHoleSummary | network | yes | no | total query error, hard-coded chart colors |
| dnsHoleControls | network/security | yes | no | total error and permission matrix |
| smartHome-entityState | smart home | yes | no | raw state in aria; action permissions |
| smartHome-executeAutomation | smart home | yes | no | mutation/permission/edit-mode E2E |
| stockPrice | analytics | yes | yes | total error |
| mediaServer | media | yes | no | total error and table a11y |
| calendar | calendar | yes | yes | total error; fixed HH:mm; schedule tradeoff |
| downloads | media | yes | yes | four-state/component coverage, arbitrary z-index |
| mediaRequests-requestList | media | yes | no | total error; external URLs/actions |
| mediaRequests-requestStats | media | yes | no | total error; links/colors |
| mediaTranscoding | media | yes | yes | total error and responsive table |
| mediaMissing | media | yes | yes | total error and external URL |
| minecraftServerStatus | infrastructure | yes | no | total error/malformed payload |
| networkControllerSummary | networking | yes | no | total error; WAN/unknown localization |
| networkControllerStatus | networking | yes | no | responsive rule duplication |
| rssFeed | feed | yes | yes | strong stale/partial state; locale relative time |
| bookmarks | basic/link | yes | yes | recomputation; URL contract |
| indexerManager | media | yes | no | total error; runtime action ref |
| healthMonitoring | infrastructure | yes | yes | integration query error |
| releases | infrastructure/feed | yes | yes | secret/item authorization needs E2E |
| mediaReleases | media | yes | no | total error; external URLs |
| dockerContainers | infrastructure | yes | yes | total error; z-index/table semantics |
| firewall | networking/security | yes | yes | partial-query matrix; loose router generics |
| notifications | infrastructure | yes | no | total error; untrusted href |
| systemResources | infrastructure | yes | yes | total error; previous network thread fixed |
| coolify | infrastructure | yes | yes | total error; URL validation |
| systemDisks | infrastructure | yes | yes | total error |
| timetable | calendar | yes | no | backend strong; component locale/live tests absent |
| immich-serverStats | media | yes | yes | two ignored query errors |
| immich-albumCarousel | media | yes | yes (query) | query error and nested overlay/focus E2E |
| paperlessNgx | analytics | yes | no | former error thread fixed, no regression spec |
| patchmon | infrastructure | yes | yes | total error |
| bazarr | media | yes | yes | total error |
| tracearr | analytics | yes | yes | total error |
| speedtestTracker | analytics | yes | yes | total error |
| uptimeKuma | infrastructure | yes | yes | total error |
| audioStats | media | yes | yes | good dual-error handling; partial-state tests needed |
| umami | analytics | yes | no | runtime query registration; chart a11y |
| vpn | networking/security | yes | no | total error; status/permission matrix |
| archiveTeamWarrior | infrastructure | yes | yes | suspense path; docs present |
| ups | infrastructure | yes | no | total error |
| beszelSystemTable | monitoring | yes | yes | nested chart/focus and live subscription |
| beszelSystemGrid | monitoring | yes | no | live/error UI; no focused renderer test |
| beszelAlerts | monitoring | yes | no | partial error and status semantics |
| beszelSystemStats | monitoring | yes | yes | runtime refs, locale tooltip |
| traefik | networking | yes | no | total error; bounded backend positive |
| customApi | custom content/security | yes | yes | WID-001/007/008; polling resume fixed |
## Severity-ranked findings
### WID-001 — P1 confirmed, pre-existing: Custom API actions lack resource authorization and hardened fetch policy
- **Location/claim:** `packages/api/src/router/custom-widget/custom-widget-router.ts:57-102,392-451`; “harden Custom API” and action-permission semantics.
- **Evidence/reproduction:** any authenticated account can call `customWidget.all`, learn every definition ID and URL, then call `customWidget.execute({definitionId})`. The mutation verifies neither board nor item nor an action permission. It decrypts administrator-provided secrets, uses `new URL()` only, and `redirect: "follow"`. The PR hardened `widget.customApi.getData`, but not this sibling execution path.
- **Impact/rule:** unauthorized side effects against internal services and authenticated redirect chains; broken object-level authorization. A view-only account can invoke administrator-created controls outside any board context.
- **Correction:** require `{boardId,itemId,definitionId}`; load the item and verify it is `customApi`, belongs to the board, references the definition, and the caller has an explicit execute policy. Until that policy exists, restrict to admin. Share `validateCustomApiUrl`, timeout, redirect rejection, bounded disposal and redacted errors with getData. Compatibility: old direct callers must send item/board context.
- **Tests:** procedure tests for anonymous, unrelated authenticated, view-only, modifier, admin, mismatched item/definition, disabled definition, redirects, credential URL, timeout and secret non-disclosure; E2E action button across roles.
### WID-002 — P1 confirmed, PR-introduced: manual advanced focus is not an accessible modal
- **Location:** `advanced-focus/context.tsx:194-209`, `items/item-content.tsx:113-189`.
- **Evidence:** manual mode renders an Overlay plus a `role="region"` Card. There is no FocusTrap, `aria-modal`, background inertness, scroll lock or backdrop dismissal. Only the close button is initially focused; Tab can leave the surface and activate dimmed board controls.
- **Impact/rule:** keyboard/screen-reader users lose the interaction boundary; WCAG 2.1.2, 2.4.3 and ARIA dialog pattern. Iframes/editors make escape especially likely.
- **Correction/Mantine:** render manual mode through `Modal.Root` compound parts (or ModalBase), using its portal, FocusTrap, scroll lock, Escape/return-focus and semantic content. Preserve preview mode as non-modal. Configure nested overlays and `removeScrollProps`; keep a 44px `Modal.CloseButton` and explicit title via VisuallyHidden if needed.
- **Tests:** RTL portal/focus-cycle/return tests; Playwright Tab/Shift+Tab, nested Menu/Select/Popover/editor/iframe, backdrop and every close path; reduced motion and 200% zoom.
### WID-003 — P1 probable, PR-introduced: fixed advanced surface remains under transformed Gridstack ancestry
- **Location:** `advanced-focus.module.css:35-42`; Card remains inside the Gridstack item while only the backdrop is portaled.
- **Evidence:** CSS fixed descendants establish their containing block at a transformed ancestor. Gridstack uses transforms for positioned items, so viewport-derived `left/top` can be interpreted relative to the item on affected layouts. A Portal wrapper exists only for Overlay.
- **Impact:** off-screen, clipped or incorrectly scaled focus surface across breakpoints/remounts.
- **Correction:** portal the focused surface itself (naturally achieved by WID-002) and calculate geometry in the same viewport coordinate system. Do not compensate with transform-specific offsets.
- **Tests:** real Gridstack at every layout/viewport, scrolled page, header, dynamic section and resize/remount; assert bounding rect within viewport and finite scale.
### WID-004 — P1 confirmed, regression-exposed/systemic: query failures still become valid empty/disconnected UI
- **Location:** 39 of 53 renderer query calls do not read `error`, including bookmarks, weather, calendar, media server, system resources, Coolify, DNS Hole, Tracearr, notifications, Immich, VPN and others listed in the matrix. Query defaults do not set `throwOnError` (`trpc.tsx:69-93`).
- **Evidence:** TanStack Query returns `data=undefined` on total failure. Components default to `[]`, `null`, or empty branches, so the board ErrorBoundary is never reached. Network Controller and Paperless were fixed, proving the pattern; it was not completed.
- **Impact:** operators are told “empty” or “disconnected” when monitoring failed, violating the product’s truthful-state requirement.
- **Correction/Mantine:** introduce a shared `WidgetQueryState` contract distinguishing initial Skeleton, background LoadingOverlay, stale Alert, partial warning, true EmptyState and terminal error. Either explicitly throw only when no usable cached data or render a shared error Alert. Preserve stale data.
- **Tests:** parameterized contract tests for every queried widget: initial, success-empty, total failure, stale+failure, partial failure, refetch, remount/persisted cache and sibling isolation.
### WID-005 — P1 confirmed, PR-introduced/systemic: integration-provided links bypass a shared URL policy
- **Location:** examples include `media-missing/component.tsx:291`, `notifications/component.tsx:77`, media requests list/stats, Coolify resource/server links, releases, bookmarks and iframe. Only RSS and custom JSX visibly use explicit protocol sanitizers.
- **Evidence:** dynamic strings are assigned directly to `href`/new-tab controls. Integration APIs and remote feeds are trust boundaries; no renderer-wide `http/https` guard is enforced.
- **Impact:** unsafe schemes, credential-bearing URLs, internal detail disclosure and inconsistent opener behavior.
- **Correction:** add one typed application URL helper returning a sanitized URL/undefined, with allowed schemes per use case, credential stripping/rejection, consistent `noopener noreferrer`, and disabled non-link presentation. Validate again server-side for action endpoints.
- **Tests:** `javascript:`, `data:`, malformed, credential, relative, http/https and missing URLs for each shared link primitive.
### WID-006 — P2 confirmed, PR-introduced: runtime query/action state mutates untyped refs during render
- **Location:** `definition.ts:121-162`; runtime registration calls in media missing/transcoding, audio, calendar, Umami, Beszel and Immich; `widget-context-menu.tsx:157-174`.
- **Evidence:** `setWidgetRuntimeQueries` mutates a shared object during render; concurrent/aborted renders can publish uncommitted query identity. Action dispatch casts the definition to `Record<string,unknown>` and `any`, then casts its result back.
- **Correction:** make definition generics preserve kind/options/action types end-to-end; replace stringly opaque ref slots with a typed per-item registration API updated in a committed effect (or use `useSyncExternalStore`). Separate query registration from imperative actions.
- **Tests:** StrictMode, interrupted/remount, options change and cleanup; compile-time `tsd`/type assertions rejecting wrong action props.
### WID-007 — P2 confirmed, regression-exposed: raw backend errors are exposed to users and assistive text
- **Location:** Custom API UI `component.tsx:561-576`; API `custom-api.ts:79-85`; several partial fallbacks use `error.message`.
- **Impact:** internal hostnames, request paths and implementation details can appear in tooltips/aria labels and logs.
- **Correction/Mantine:** return stable error codes plus redacted public messages; keep detailed causes server-side with correlation IDs. Render localized Alert/Notification details, never raw throwable text.
- **Tests:** errors containing credentials, query strings, private hosts and multiline backend bodies must not reach tRPC payload, DOM, accessibility tree or notifications.
### WID-008 — P2 confirmed, PR-introduced/regression-exposed: locale handling remains incomplete
- **Locations:** Beszel chart/tooltip dayjs formats, calendar event `HH:mm`, downloads `YYYY-MM-DD HH:mm`, timetable `HH:mm:ss`, weather day/month/time formats; `_inputs/widget-location-input.tsx:199` hardcodes `Intl.NumberFormat("en")`.
- **Also:** `beszel/_shared/disk-usage.tsx:41`, stats loading aria label, Network Controller `WAN`/`unknown` are English/raw accessible strings.
- **Correction:** centralize app-locale date/time/number/duration/status helpers; load canonical dayjs locale only at the boundary; use localized translation keys for aria/status. NumberFormatter is suitable for simple display but application helpers should own locale.
- **Tests:** at least `en-US`, `fr-FR`, `de-DE`, RTL locale, 12/24-hour preference and long translations, including accessibility names.
### WID-009 — P2 confirmed: layer and color tokens are inconsistent
- **Locations:** downloads z-index 1000 and rgba row colors; item title z-index 4; item menu/dynamic menu 10; media server white rgba; Indexer Manager hex colors; DNS Hole rgba palette; Umami hex tick colors.
- **Correction:** define shared semantic layer tokens relative to Mantine CSS z-index variables and semantic status/chart tokens respecting light/dark/forced colors. Do not replace intentional service branding.
- **Tests:** light/dark/forced-colors with nested overlays and tooltip ordering.
### WID-010 — P2 confirmed: responsive decisions are duplicated and under-tested
- **Location:** Network Controller Status `component.tsx:44,59-78` (open valid thread); Bookmark `component.tsx:94` recomputation (valid but low-impact thread); similar width/height literals across renderers.
- **Correction:** small typed breakpoint lookup helpers per family, memoized only for genuinely expensive layout. Prefer container-query Grid/SimpleGrid where structural. Do not cargo-cult `useMemo` for a cheap pure calculation; benchmark Bookmark before accepting the review suggestion.
- **Tests:** boundary tables for minimum/typical/wide/short/advanced sizes and 200% zoom.
### WID-011 — P2 confirmed: PR/docs overstate behavior and validation
- **Location:** PR body, board/getting-started copy “advanced preview in place,” validation numbers, and unqualified review pass.
- **Correction:** state 53 advanced renderers plus compact-only app; call manual focus an overlay only after WID-002/003; publish exact final-SHA check/test counts and disclose unsupported/live-untested states.
### WID-012 — P2 confirmed: focused automated coverage is far below the 54-widget/data-state claim
- **Evidence:** 42 widget-local spec files are helper-heavy; 20+ renderers have no focused spec. Six E2E files exist, but only board interactions and Custom JSX directly exercise this modernization. The earlier Custom JSX login wait failed once and current exact-route wait remains.
- **Correction:** add shared renderer contract harness plus family E2Es. Replace Custom JSX’s exact post-submit URL race with a shared login helper that waits for authenticated session/stable destination, then run it three times in CI.
### WID-013 — P2 confirmed: open review-thread state no longer matches code
| Thread | Classification |
| ------------------------------------------- | ------------------------------------------------------------------- |
| iframe complete URL | already fixed/false positive at current line: only host is rendered |
| system resources network gate | already fixed, outdated |
| RSS total error | already fixed: no-data error throws; stale data retained |
| bookmarks memoization | valid P3/P2-low; benchmark first |
| network-controller total error | already fixed, outdated |
| network-controller breakpoint consolidation | valid P2 maintainability |
| Paperless total error | already fixed, outdated |
Resolve the five stale threads with evidence; address or explicitly disposition the two live suggestions. Re-run review because CodeRabbit was paused during active development.
## Systemic remediation roadmap
1. **Security/authorization:** WID-001 and WID-005; add negative-role/protocol tests before merge.
2. **Interaction accessibility:** replace manual focus with portaled Mantine modal semantics (WID-002/003), then test nested widget content.
3. **Truthful data state:** build the shared query-state contract and migrate all 39 ambiguous sites; keep valid stale data and partial results.
4. **Typed interfaces:** replace `widgetStateRef` slot mutation and `any` context dispatch with committed typed registrations.
5. **Widget families:** media/downloads, infrastructure, networking/security, smart-home, analytics/calendar/feed, then Beszel; apply URL, state, locale, responsive and a11y contracts in that order.
6. **Design-system cleanup:** semantic layers/colors and restrained Mantine consolidation. Do not add Schedule or upgrade to 9.5 without a separate dependency/bundle decision.
7. **Docs/PR:** correct renderer count, behavior and exact validation evidence.
8. **Final proof:** frozen install on supported Node/pnpm; format, lint, full typecheck, affected and full unit tests, production and docs builds; Custom JSX three times; complete production-container E2E; desktop/tablet/mobile keyboard/touch/light/dark/reduced-motion/zoom/a11y-tree QA. Push nothing until all refer to one unchanged SHA; close every review thread and obtain required approval.
## Positive findings
- Timetable is the strongest security work in the PR: saved resource authorization, private-range rejection, DNS rebinding resistance through pinned lookup, redirect rejection, bounds and schemas are coherently tested.
- Bounded response helpers, Beszel bounded queue/cancellation, and partial-integration settlement materially reduce failure blast radius.
- RSS preserves stale data and distinguishes partial feed failure; Network Controller and Paperless now correctly escalate total failures.
- Item-scoped query matchers and tests address sibling-widget refresh isolation; global polling defaults are restricted to the widget namespace.
- Touch long-press has movement tolerance; focus scale guards prevent Infinity/NaN; reduced-motion and remount cleanup are present.
- Existing Homarr theme, Mantine components and shared UI vocabulary are generally preserved. Most proposed Mantine work is consolidation, not redesign.
## Validation evidence and uncertainty
- `MANTINE.md` was fetched from `https://mantine.dev/llms.txt` and passed byte-for-byte `cmp`; `llms-full.txt` stayed in `/tmp`.
- Context7 current Mantine docs were queried for overlay/focus, responsive/data-state/accessibility, charts and Schedule. Static 9.4.1 archives verified exact exports; Cascader, BulletChart and SunburstChart are 9.5-only.
- Local frozen install could not start because host Node/pnpm are below repository engine requirements. No dependencies, source files or machine toolchain were changed.
- Docker API access is denied, so production-container E2E could not be rerun locally. Hosted exact-head Fast gate and Container/E2E are green.
- `agent-browser` is not installed. Temporary `npx` execution was rejected by the environment’s security reviewer, so live exploratory QA/a11y-tree inspection was not performed.
- External integrations were not available; malformed/partial/permission combinations are static-analysis conclusions unless a focused test already exists.
- This review intentionally changes no production code and makes no commit, push, thread resolution or PR edit.
Finalization must re-query the PR head/checks/threads. If the head differs from the reviewed SHA, this report is a snapshot and the delta requires review before merge.
**Final refresh (2026-08-03):** head remained `96e92485a5fd45f3648cfac0453e614779c414ba`; all reported hosted checks remained successful; merge state remained `BLOCKED` with no approval decision; unresolved threads remained 7 of 34. The branch did not change during the audit.
@@ -7,39 +7,15 @@ tags:
Homarr has the following keyboard shortcuts: Homarr has the following keyboard shortcuts:
| Shortcut | Description | | Shortcut | Description |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | ----------- | -------------------- |
| `MOD` + `K` | Opens the search bar | | `MOD` + `K` | Opens the search bar |
| `MOD` + `J` | Toggles Dark Mode | | `MOD` + `J` | Toggles Dark Mode |
| `MOD` + `E` | Toggles Edit Mode | | `MOD` + `E` | Toggles Edit Mode |
| Hold `Shift` for 500 ms over a supported widget | Opens its temporary advanced view |
| `Shift` + `Enter` on a supported widget | Opens its advanced view until it is closed |
| `Esc` | Stops keyboard tile editing, cancels an active pointer or touch move/resize, or closes an open advanced view or board menu |
## Board editing
`MOD` + `E` enters edit mode. Homarr keeps the current board visible while the editor prepares; movement and resizing become available when it is ready.
When a tile is focused in edit mode:
| Shortcut | Description |
| -------------------- | --------------------------------------------------------------------------- |
| `Enter` or `Space` | Starts or stops keyboard editing |
| Arrow keys | Moves the focused tile within its current canvas, rail, or Container |
| `Shift` + Arrow keys | Resizes from the tile's lower-right corner: Left/Up shrink; Right/Down grow |
| `Esc` | Stops keyboard editing without undoing its changes |
Keyboard moves and resizes apply immediately. To move an item between a canvas, rail, or Container, drag it there or use **Move / resize item** for precise numeric placement.
Commands at a grid boundary or at the tile's minimum size are ignored and announced instead of moving or shrinking the tile.
On touch screens, press and hold a tile before moving it; a quick tap does not start a drag. Resize handles accept direct touch input. During a pointer or touch move or resize, `Esc` cancels the interaction and restores the layout from before it started.
:::tip :::tip
Here `MOD` refers to the modifier key, which is `Ctrl` on Windows and `Cmd` on macOS. Here `MOD` refers to the modifier key, which is `Ctrl` on Windows and `Cmd` on macOS.
Advanced view is available outside edit mode. Widgets without a distinct advanced experience adapt to their normal
dimensions instead. A progress ring follows the pointer while the temporary advanced view is preparing to open.
::: :::
:::note :::note
Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 39 KiB

+2 -2
View File
@@ -85,8 +85,8 @@ To do this, enter the board settings:
![](./img/enter-board-settings.png) ![](./img/enter-board-settings.png)
Scroll to the **Custom CSS** section on the board settings page. It is part of the unified settings form; enter your CSS Scroll down a bit and click on custom CSS.
there and save the board settings to apply it to that board. Here you can enter your CSS code, which will be applied on the respective board page.
![](./img/custom-css-setting.png) ![](./img/custom-css-setting.png)
@@ -10,16 +10,10 @@ import imageStartFromScratchStepServerSettings from "./img/onboarding-from-scrat
import imageStartFromScratchStepAdminUser from "./img/onboarding-from-scratch-step-admin-user.png"; import imageStartFromScratchStepAdminUser from "./img/onboarding-from-scratch-step-admin-user.png";
import imageStartFromScratchStepExternalAdminGroup from "./img/onboarding-from-scratch-step-external-admin-group.png"; import imageStartFromScratchStepExternalAdminGroup from "./img/onboarding-from-scratch-step-external-admin-group.png";
import imageStartFromScratchStepFinish from "./img/onboarding-from-scratch-step-finish.png"; import imageStartFromScratchStepFinish from "./img/onboarding-from-scratch-step-finish.png";
import manageBoardHeaderEditMode from "./img/manage-board-header-edit-mode.png";
import manageBoardHeaderAddItem from "./img/manage-board-header-add-item-menu.png";
import manageBoardHeaderChooseItem from "./img/manage-board-header-choose-item.png";
import manageBoardNoApp from "./img/manage-board-no-app.png"; import manageBoardNoApp from "./img/manage-board-no-app.png";
import currentKeyboardTileEditing from "../management/boards/img/keyboard-tile-editing.png";
import boardDesktopCurrent from "./img/onboarding-board-desktop-current.png";
import boardEditModeCurrent from "./img/onboarding-board-edit-mode-current.png";
import addContentMenuCurrent from "./img/onboarding-add-content-menu-current.png";
import chooseItemCurrent from "./img/onboarding-choose-item-current.png";
import editItemCurrent from "./img/onboarding-edit-item-current.png";
import boardsHomeMobileMenu from "./img/onboarding-boards-home-mobile-menu.png";
import moveResizeMenuCurrent from "./img/onboarding-move-resize-menu-current.png";
import currentBoardMobileView from "./img/onboarding-board-mobile-current.png";
import manageExportOldHomarr from "./img/old-homarr-export-data.png"; import manageExportOldHomarr from "./img/old-homarr-export-data.png";
import manageImportOldHomarrDropzone from "./img/old-homarr-import-dropzone.png"; import manageImportOldHomarrDropzone from "./img/old-homarr-import-dropzone.png";
import manageImportOldHomarrChooseData from "./img/old-homarr-import-choose-imported-data.png"; import manageImportOldHomarrChooseData from "./img/old-homarr-import-choose-imported-data.png";
@@ -35,12 +29,12 @@ Congratulations on installing Homarr. Now it's time to get yourself familiar wit
## Onboarding ## Onboarding
The onboarding will show the first time you start Homarr. 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. This process will help you with your first steps and adding some initial data, such as the administrator account.
:::note :::note
If you see this screen repeatedly after you restart the Homarr container, your Docker mounting paths may be missing, If you see this screen repeatitly after you restarted the Homarr container, your Docker mounting paths may be missing,
incorrect or read-only. Read [our installation instructions](/docs/getting-started/installation/docker) again to fix this issue. incorrect or read-only. Read [our installation instructions](/docs/getting-started/installation/docker) again to fix this issue.
::: :::
@@ -74,7 +68,7 @@ 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. 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 completely anonymous and fully compliant with GDPR and PECR. Here you can also disable analytics. Please note that our analytics are completly anonymous and is fully compliant with GDPR and PECR.
<img <img
src={imageStartFromScratchStepServerSettings} src={imageStartFromScratchStepServerSettings}
@@ -132,20 +126,18 @@ It is important that you first migrate correctly using [the list of breaking cha
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. 2. Check the contents that you want to export. We recommend you to export all data.
3. Click on the export data button 3. Click on the export data button
4. Save the downloaded ZIP file on your device. If you selected "integrations" or "users", you also get an encryption / decryption key. 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 keep it safe; you need it later in the import flow. Copy it and do not loose it!
5. Go to your new Homarr 1.0 or later instance and start the onboarding. Choose `Import from Homarr before 1.0`. 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: <br/><img src={manageImportOldHomarrDropzone} style={{ maxWidth: 500 }} /> 6. Drag and drop your previously downloaded ZIP archive into the file upload: <br/><img src={manageImportOldHomarrDropzone} style={{ maxWidth: 500 }} />
7. Next, choose the data that you want to import and how to import it. Old Homarr screen sizes are imported as responsive layouts on the same board: the narrowest becomes **Mobile**, the widest becomes **Base**, and layouts between them become custom layouts. Homarr normalizes duplicate breakpoints automatically, so you can review the imported board in Board Settings and adjust the layouts there.<br/><img src={manageImportOldHomarrChooseData} style={{ maxWidth: 500 }} /> 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.<br/><img src={manageImportOldHomarrChooseData} style={{ maxWidth: 500 }} />
For example, legacy `sm`, `md`, and `lg` layouts at `0px`, `800px`, and `1400px` become Mobile, a custom layout, and Base. The imported board keeps each layout's positions, names, and column counts where possible so you can review the result before saving. If a board has one saved layout, Homarr keeps it as Base and generates Mobile from it; a board without saved layouts receives Mobile at `0px` with 3 columns and Base at `768px` with 10 columns.
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.<br/><img src={manageImportDecryptionToken} style={{ maxWidth: 500 }} /> 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.<br/><img src={manageImportDecryptionToken} style={{ maxWidth: 500 }} />
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. 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 ## Creating your first board
In Homarr, you organize your apps and widgets on boards. In Homarr, you organize your apps and widgets on boards.
Boards use fixed-size tiles so the same widget keeps the same proportions on every screen. Boards can be organized using the interactive drag and drop.
Drag, resize, and section controls are available in edit mode.
Let's create your first board by navigating here: `Manage` > `Boards` > `New board`: Let's create your first board by navigating here: `Manage` > `Boards` > `New board`:
![](./img/manage-create-board-button.png) ![](./img/manage-create-board-button.png)
@@ -154,25 +146,9 @@ Public boards are also accessible for users, that are not logged in ([see access
![](./img/manage-create-board-modal.png) ![](./img/manage-create-board-modal.png)
Congratulations - you've just created your first board. You can see the boards you have access to on this page: Congratulations - you've just created your first board. You can see boards, that you have access to, on this page:
<figure> ![](./img/manage-view-board.png)
<img
src={boardsHomeMobileMenu}
alt="Boards management page showing a Home badge and separate desktop and mobile home-board menu entries"
style={{ maxWidth: "100%" }}
/>
<figcaption>The Boards page selects the board opened from Home separately for desktop and mobile.</figcaption>
</figure>
The board marked **Home** is the board opened from the desktop Home route. **Set as your mobile board** selects which board
opens on phones. This is separate from the **Mobile** layout: the home-board setting chooses a board, while **Mobile** and
**Base** are responsive layouts inside that board. A phone can therefore open a different home board and still use that
board's Mobile layout.
New boards start with **Mobile** and **Base** responsive layouts automatically, so you can edit the board for phones and
desktops without creating extra layouts first. Mobile starts at `0px` with 3 columns and Base at `768px` with 10 columns.
Each layout keeps its own tile positions and sizes.
## Add your first app to Homarr ## Add your first app to Homarr
@@ -196,63 +172,32 @@ After you created the app, you'll see in on the apps page:
Let's add your freshly created app, from the previous step, to the 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:
<figure> ![](./img/manage-view-board.png)
<img src={boardDesktopCurrent} alt="Current Homarr board opened from the Boards page" style={{ maxWidth: "100%" }} />
<figcaption>Open a board to view its dashboard, then enter edit mode to arrange its content.</figcaption>
</figure>
Here you have the following options in the header: Here you have the following options in the header:
![](./img/manage-board-header.png) ![](./img/manage-board-header.png)
- **Search**: Here you can search for data. You can search for integrations, boards, actions, sites and much more. Some actions may be context dependent (eg. will only show when certain widgets are on your page) - **Search**: Here you can search for data. You can search for integrations, boards, actions, sites and much more. Some actions may be context dependent (eg. will only show when certain widgets are on your page)
- **Edit board**: Clicking this button will toggle edit mode. In this mode you can edit the contents of your board. - **Edit mode**: Clicking this button will toggle the edit mode. In this mode you can edit the contents of your board.
- **Settings**: Clicking this button will redirect you to a settings page where you can adjust the board settings (page title, colors, ...). - **Settings**: Clicking this button will redirect you to a settings page where you can adjust the board settings (page title, colors, ...).
- **Board switcher**: Use this button to open another board that you can access.
- **Profile**: This avatar will be visible on every page. Clicking it will show a menu with actions. - **Profile**: This avatar will be visible on every page. Clicking it will show a menu with actions.
When entering the edit mode, you'll get some additional buttons to work with: When entering the edit mode, you'll get some additional buttons to work with:
<figure> <img src={manageBoardHeaderEditMode} alt={"Screenshot of buttons in the edit mode"} width={200} />
<img
src={boardEditModeCurrent}
alt="Current Homarr board editor with tile controls visible"
style={{ maxWidth: "100%" }}
/>
<figcaption>The current editor keeps the board visible while its editing controls are available.</figcaption>
</figure>
- **Add board content**: Click this button to create a new item, app, integration, or Container. - **Add item**: Click this button to add new elements to your page.
- **Save board**: Click this button to save your changes and leave edit mode. If you leave with unsaved changes, Homarr asks whether to discard them. - **Exit edit mode**: Click this btuton to save your changes and exit edit mode.
Outside edit mode, right-click a widget for its live status, refresh, settings, quick options, and supported actions. To continue, click the option "New Item":
Hold `Shift` while hovering a widget that supports advanced view to expand it in place while the rest of the board
stays visible but dimmed. Release `Shift` to close it. You can also focus that widget and press `Shift` + `Enter` to
keep its advanced view open until you close it. Widgets without a distinct advanced experience adapt in place.
To continue, click **New item** in the add-content menu. New content is placed at the first available position on the main <img src={manageBoardHeaderAddItem} alt={"Screenshot of the add item menu"} width={200} />
canvas; it is not inserted at a clicked empty cell or directly into a rail or Container:
<figure>
<img
src={addContentMenuCurrent}
alt="Current add board content menu with item, app, integration, and Container actions"
style={{ maxWidth: "100%" }}
/>
<figcaption>The add-content menu provides the four supported ways to place new content on the board.</figcaption>
</figure>
Next, you can choose what item you'd like to add. 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. We will discuss the other options in the following sections - for now choose "App" to continue.
<figure> <img src={manageBoardHeaderChooseItem} alt={"Screenshot of choosing the app item in the modal"} width={300} />
<img
src={chooseItemCurrent}
alt="Current item picker showing the App card and other item types"
style={{ maxWidth: "100%" }}
/>
<figcaption>Choose the App item from the current picker to add an app tile.</figcaption>
</figure>
As soon as you chose the app, the modal will close and you'll get the following item on your board: As soon as you chose the app, the modal will close and you'll get the following item on your board:
@@ -261,57 +206,28 @@ As soon as you chose the app, the modal will close and you'll get the following
This is expected, because you haven't told Homarr what app it should show in this item. 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. 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.
<figure> ![](./img/manage-board-edit-app.gif)
<img
src={moveResizeMenuCurrent}
alt="Current board tile menu showing Edit item and Move or resize item"
style={{ maxWidth: "450px" }}
/>
<figcaption>The tile menu contains the item editor and the separate move-and-resize tools.</figcaption>
</figure>
In the modal you can edit the app that you want to display: In the modal you can edit the app that you want to display:
<figure> ![](./img/manage-board-choose-app.png)
<img src={editItemCurrent} alt="Current Edit item modal for an App tile" style={{ maxWidth: "620px" }} />
<figcaption>Use the Edit item modal to choose the app and configure how its tile is displayed.</figcaption>
</figure>
As soon as you save your changes, the tile on the board will refresh and display your app. As soon as you save your changes, the tile on the board will refresh and display your app.
Resize from any visible edge or corner handle. For a Container, activate its top-left grip before using its resize handles. Using the small bidirectional arrow at the bottom right you can resize the element.
Press and hold a handle, move the pointer, and release it when the tile has the size you want. Start pressing with your mouse button down, move your mouse to resize and release your finger as soon your you're happy with the size.
To move a tile in edit mode, press and hold its inert content area, move it to the desired position, and release it. ![](./img/resize-item.gif)
Widget content is disabled while editing so its controls do not intercept the drag; use the tile's settings button for edit actions.
The tile snaps to the grid. A single compatible collision may swap the two items when both resulting placements fit;
otherwise the collision chain reflows neighboring tiles automatically.
Every `1 × 1` tile has a logical content size of `200 × 200` pixels. To move your item to a different position on the board, simply hold the mouse button anywhere on the item and move with the mouse to the desired position.
Larger tiles use multiples of that fixed unit, with a consistent gap between cells. As soon as you release your finger, the item will snap in place in the grid.
Homarr scales the complete board uniformly, like zooming a page.
The complete canvas always fits the available width without horizontal board scrolling. Below 100%, Homarr preserves the
widget's readable typography and control density instead of shrinking each widget independently.
It does not squeeze or reflow the content of individual widgets, so a widget keeps the same design and proportions across screen sizes.
On a narrow screen, Homarr uses the **Mobile layout** for the selected board and stacks the board content vertically while ![](./img/move-item.gif)
preserving each widget's fixed logical proportions:
<figure>
<img
src={currentBoardMobileView}
alt="Homarr's Mobile layout in a 390-pixel-wide viewport, with tiles scaled to fit and stacked vertically"
style={{ maxWidth: "390px" }}
/>
<figcaption>
At phone width, the selected board uses its Mobile layout and fits the complete canvas to the viewport.
</figcaption>
</figure>
:::note :::note
Viewing a board initially uses the lightweight, non-interactive layout. You cannot move items freely on the vertical axis.
For users with edit access, Homarr prepares drag-and-drop and resize controls in the background after the board becomes interactive. We always align all items to the top of the page to simply organization.
If you enter edit mode before that finishes, the existing board stays visible until the editor is ready. To move items to the bottom, you must add more items or resize items.
::: :::
@@ -322,17 +238,14 @@ This helps you to explore the capabilities of Homarr before you build your board
::: :::
Keyboard users can focus a tile, press Enter or Space, then use arrow keys to move and Shift plus arrow keys to resize Users on touch or mobile devices may not be able to drag and drop or move items.
within its current canvas, rail, or Container. Press Escape to stop keyboard editing. Touch dragging starts after a short They can use the "Move / resize item" option in the edit menu of items instead:
hold of about `200 ms`; resize handles accept direct touch input. During a pointer or touch move or resize, press Escape
to cancel and restore the previous layout. For cross-grid moves or precise numeric controls, choose **Move / resize item**
from the tile's edit menu.
Keyboard commands at a grid boundary or minimum tile size are ignored and announced; they do not move or shrink the tile.
<img src={currentKeyboardTileEditing} alt="A board tile focused for keyboard editing" style={{ maxWidth: "100%" }} /> ![](./img/manage-board-move-resize-item-menu.png)
Open **Move / resize item** from the tile menu to move the tile to another section or rail and enter precise numeric Be sure to save your changes by exiting edit mode!
coordinates and dimensions. After making changes, click **Save board** to persist them and leave edit mode.
![](./img/exit-edit-mode.png)
## Adding your first integration ## Adding your first integration
@@ -367,14 +280,7 @@ Let's add your integration, that you have configured in the section above, as a
First, navigate back to your board that you want to add it to. 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: Next, enter edit mode again and add another item to your board:
<figure> ![](./img/manage-board-header-add-item-menu.png)
<img
src={addContentMenuCurrent}
alt="Current add board content menu with item, app, integration, and Container actions"
style={{ maxWidth: "100%" }}
/>
<figcaption>Use the same add-content menu to place an integration widget on the board.</figcaption>
</figure>
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": Click on "Add to board":
@@ -401,38 +307,47 @@ Editing a widget will trigger a data update and data will soon appear:
![](./img/widget-system-health.png) ![](./img/widget-system-health.png)
As with every change, click **Save board** to persist your board changes and leave edit mode. As with every change, don't forget to save your changes using the exit edit mode button:
## Organizing items with Containers ![](./img/exit-edit-mode.png)
Containers are resizable areas for apps, widgets, and nested Containers. ## Grouping your items
The same Container works on the main canvas, inside a fixed rail, or inside another Container.
Each Container can configure:
- Its label and whether that label is visible. As the last step, let's organize your freshly created items using two different options.
- Allow or disable collapsing. You can choose either or both of them on your boards.
- Show or hide **Open all in new tabs**.
- Change its border color and custom CSS classes.
Open the add-content menu and choose **New container**. ### Using dynamic sections
In edit mode, move and resize it like any other item, then drag items into or out of it.
Nested content stays within the Container's bounds.
While resizing, the solid outline follows the pointer continuously and the dashed grid preview shows the size that will be saved on release.
### Collapsing and opening apps Dynamic sections are a simple, resizable visual group to group items.
Users can collapse an eligible Container like an accordion in view mode without changing its saved expanded size. ![](./img/create-dynamic-section.png)
The full-width header becomes the compact expansion bar, so the half-row state has no empty pill-shaped spacer.
Expanding it restores its bounds and makes room for its contents.
If **Open all in new tabs** is enabled, the Container menu can open every app link it contains.
### Fixed rails After adding a dynamic section, you can simply drag and drop items into the section:
Open the board's **Layout** settings to enable the left or right rail for each responsive layout. ![](./img/dynamic-section.gif)
Choose one, two, or three columns with the slider and use the preview to check the remaining canvas width.
The rail reserves those columns from the dashboard, so Homarr moves canvas items into the next available positions. ### Using categories
Rail content stays visible while the page scrolls and remains editable.
Each responsive layout keeps its own rail widths, item positions, and item sizes. Categories are more advanced and complicated than dynamic sections.
To create a category, open the add item modal and choose "new category":
![](./img/add-category.png)
Every category must have a name. After you click "Add category", you will get a new item on your board:
![](./img/category.gif)
You can drag and drop items in to this category in the edit mode.
Categories will have a big space before and after them in the edit mode to
enable you to drag and drop items outside the category.
You can also collapse or open categories in edit mode to facilitate your editing process.
:::note
A current limitation of categories is, that they will always have a width of 100%.
You cannot reduce their horizontal width.
:::
## Guided tours ## Guided tours
+8 -8
View File
@@ -1,13 +1,13 @@
# Glossary # Glossary
This glossary explains Homarr's main components and the terms used for them. In the glossary, you'll learn the different components and their names of Homarr.
| Term | Definition | | Term | Definition |
| ----------- | --------------------------------------------------------------------------------------- | | --------------- | -------------------------------------------------------------------------------------------------------------- |
| Apps | A piece of software that you or a third party hosts (for example, Google) | | Apps | An piece of software that you or a third party hosts (eg. Google) |
| Integration | A connection to a service outside of Homarr | | Integration | A connection to a service outside of Homarr |
| Container | A resizable area that can hold apps, widgets, and nested Containers | | Dynamic section | Resizeable area where items on your board can be placed and grouped |
| Widget | A tile that uses your integrations to display data or enables you to interact with them | | Widget | A tile that uses your integrations to display data or enables you to interact with with them |
| Edit mode | A temporary state where you can drag, resize, edit, or customize your board | | Edit mode | Temporary state that you can enter where you can drag and drop, edit or customize your board |
| Board | A customizable page where apps, widgets, and Containers are arranged | | Board | Customizable page which can be edited using the edit mode. This is where apps, sections and widgets are placed |
| Tasks | Workloads that are performed in the background, even if no user is present | | Tasks | Workloads that are performed in the background - even if there is no user present |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 561 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

@@ -2,9 +2,9 @@
<img src="https://raw.githubusercontent.com/homarr-labs/charts/refs/heads/main/charts/homarr/icon.svg" align="right" width="92" alt="homarr logo"> <img src="https://raw.githubusercontent.com/homarr-labs/charts/refs/heads/main/charts/homarr/icon.svg" align="right" width="92" alt="homarr logo">
![Version: 8.26.0](https://img.shields.io/badge/Version-8.26.0-informational?style=flat) ![Version: 8.28.1](https://img.shields.io/badge/Version-8.28.1-informational?style=flat)
![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat)
![AppVersion: v1.74.0](https://img.shields.io/badge/AppVersion-v1.74.0-informational?style=flat) ![AppVersion: v1.76.1](https://img.shields.io/badge/AppVersion-v1.76.1-informational?style=flat)
A Helm chart to deploy homarr for Kubernetes A Helm chart to deploy homarr for Kubernetes
@@ -437,7 +437,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 | | httproute.rules[0].matches[0].path.value | string | `"/"` | Path value to match |
| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | | image.pullPolicy | string | `"IfNotPresent"` | Image pull policy |
| image.repository | string | `"ghcr.io/homarr-labs/homarr"` | Image repository | | image.repository | string | `"ghcr.io/homarr-labs/homarr"` | Image repository |
| image.tag | string | `"v1.74.0"` | Overrides the image tag whose default is the chart appVersion | | image.tag | string | `"v1.76.1"` | Overrides the image tag whose default is the chart appVersion |
| imagePullSecrets | list | `[]` | Secrets for Docker registry | | imagePullSecrets | list | `[]` | Secrets for Docker registry |
| ingress.annotations | object | `{}` | Ingress annotations | | ingress.annotations | object | `{}` | Ingress annotations |
| ingress.enabled | bool | `false` | Enable ingress | | ingress.enabled | bool | `false` | Enable ingress |
@@ -19,6 +19,8 @@ import Alert from "@theme/Admonition";
<IntegrationCapabilites items={[{ widget: healthMonitoringWidget }, { widget: systemResourcesWidget }]} /> <IntegrationCapabilites items={[{ widget: healthMonitoringWidget }, { widget: systemResourcesWidget }]} />
The storage values shown by these widgets come from each pool's root ZFS dataset, so they already account for RAIDZ parity overhead and dataset limits and match what `zfs list` and the TrueNAS web interface report. When a pool cannot report its dataset space, Homarr falls back to the pool's physical values, which read higher on RAIDZ.
### Adding the integration ### Adding the integration
<AddingIntegration /> <AddingIntegration />
-20
View File
@@ -82,28 +82,8 @@ The OpenAPI and MCP APIs include board management endpoints for common automatio
- Set desktop and mobile home boards for the current user - Set desktop and mobile home boards for the current user
- Configure global board defaults such as public home boards and status behavior - Configure global board defaults such as public home boards and status behavior
Board layout responses identify each layout with a `mobile`, `base`, or `custom` role. Mobile and Base are protected. Layout-saving clients must preserve existing roles, keep Mobile at breakpoint `0`, keep Base as the highest breakpoint, and use unique custom breakpoints between them. The layout save operation returns the canonical saved layouts, including server-generated IDs for new custom layouts. A non-Base layout can be regenerated from Base with the protected reset operation.
Use the interactive API reference to inspect the exact request body for each endpoint under the `boards` and `settings` tags. Use the interactive API reference to inspect the exact request body for each endpoint under the `boards` and `settings` tags.
## Partial integration failures
Multi-integration widget queries keep data from integrations that remain available. Calendar and media-server results include
an optional `error` on a failed integration; media-request responses expose a `failedIntegrations` array, and the latest
requests endpoint returns its items in `requests`. When every selected integration fails, the query returns an error instead
of a partial response.
Other widgets use the same settlement pattern with endpoint-specific shapes: a failed integration may appear as a named
fallback row, in a failure array, or be omitted while healthy results remain. Widget pages should identify the shape they
render rather than assuming every query exposes an `error` field.
### Saving dashboard layouts
The dashboard editor saves item positions and sections as one board update.
Canvas height follows its content automatically and does not require a separate API setting.
Responsive layouts store optional left and right gutter widths from zero to three columns.
Gutter columns are reserved from the layout's total column count, and changing them reflows affected items.
## Get Custom Widget authoring resources ## Get Custom Widget authoring resources
Custom JSX v2 provides four HTTP discovery endpoints: Custom JSX v2 provides four HTTP discovery endpoints:
-362
View File
@@ -1,362 +0,0 @@
---
tags:
- Assistant
- AI
- Administration
- Security
---
import assistantConfigImage from "./img/assistant-config.png";
import assistantWidgetImage from "./img/assistant-widget.png";
# Assistant
Administrators can configure the instance-wide AI assistant from **Management → Assistant**.
It is available to signed-in users from the user menu, with `Shift + A`, and from the
`Cmd/Ctrl + K` command menu. **Ask Homarr Assistant** is always available as a local result in the default
search. Selecting it with an existing query sends that query in the background; selecting it without a
query turns the command-menu search field into a prompt composer. Press
`Enter` to send the prompt in the background without opening the conversation. The search field receives
focus whenever the command menu opens, including when it is opened by an assistant tool. Sending from the
command menu preserves any draft in the expanded conversation. The local result is temporarily disabled while
the assistant is responding or waiting for an approval.
Availability follows the setup state. Signed-out users do not get an assistant action in the user menu or from
`Shift + A`; the always-visible command-menu result is disabled and says **Sign in**. If the provider connection or
model is configured but **Enable Assistant** is off, the user-menu and `Shift + A` entry points do not open the
assistant, while `Cmd/Ctrl + K` keeps its result visible but disabled with an explanation that an administrator must
configure and enable it. Saving a working connection, model, and enabled state makes all three entry points
available again.
While a response is running, a compact activity bar appears at the bottom of the page and the user-menu
avatar shows a thinking indicator. The activity bar can be opened to follow the response. Completed
responses add an unread count to the user menu until they are opened or dismissed. The expanded
conversation has a minimize action that returns it to the activity bar without interrupting a response.
Its adjacent close action hides the panel without leaving an activity bar behind. Both window actions use
subtle buttons with an icon, tooltip, and accessible label.
The transcript follows streamed output while it is already at the bottom, including fast responses. Scrolling
up releases that follow state until **Scroll to latest** is selected or the user returns to the bottom.
`Shift + A` opens the compact conversation panel directly. The shortcut is ignored while typing in text
fields, selectors, and editable content.
The **Assistant** board widget embeds the same persisted conversation and full interaction surface directly
on a dashboard. By default it follows the current conversation. Widget settings can instead pin a specific
conversation; the conversation picker stays hidden until **Pinned conversation** is selected.
If the pinned conversation was deleted or cannot be opened, the widget shows an **Open conversation** recovery action
and retry state instead of silently losing the widget content or falling back to another thread. **Open conversation**
retries the same pinned thread after a failed switch.
<img
src={assistantWidgetImage}
alt="Assistant widget embedded on a dashboard with its conversation header, automatic approvals control, model selector, and composer"
style={{ maxWidth: "100%" }}
/>
The current preview capture shows the board-embedded surface, including its conversation header, automatic-approval
control, model and thinking controls, and composer. These controls operate on the same persisted thread as the
floating assistant while keeping this surface's draft and focus independent.
The Shift+A panel is non-modal: opening it focuses its composer, but it does not capture focus from the board.
Users can continue typing in an Assistant widget or interacting with other dashboard controls while the panel
is open. Every visible assistant surface keeps its own draft, attachment list, selection, and focus while sharing
the same persisted conversation. Switching conversations restores the draft that belongs to that surface and
conversation instead of copying text into another widget or the floating panel.
When the assistant is unavailable, its command-menu result remains visible but disabled and explains that
an administrator must configure and enable it.
The assistant uses the assistant-ui runtime for **AI SDK v7**. Homarr tracks the newest mutually compatible
package set permitted by its supply-chain release-age policy. Persisted message rows can still report the upstream
codec name `ai-sdk/v6`; assistant-ui's current v7 adapter deliberately retains that identifier for its compatible
UI-message wire format. It does not mean the application is running AI SDK v6.
## Configure a model provider
Homarr includes presets for OpenRouter, OpenAI, Anthropic Claude, Google Gemini, xAI, Groq, Mistral AI,
DeepSeek, Together AI, Ollama, and LM Studio. Provider icons make the presets easy to identify. A custom
preset connects any other service that implements the OpenAI Chat Completions API.
The provider list also shows **Homarr — Coming soon** as a disabled option.
The management page presents setup as three compact sections: provider and endpoint, credentials, then
model and availability. A status summary at the top shows whether the connection is ready, whether a key
is protected, and which model is selected. Redundant active-state badges are omitted when the setup is ready.
<img
src={assistantConfigImage}
alt="Assistant configuration page showing provider, credentials, model, and availability sections"
style={{ maxWidth: "100%" }}
/>
The screenshot shows the configuration surface with no provider key entered; the assistant remains disabled until a
working provider, model, and enabled state are saved.
1. Choose a provider preset or **Custom endpoint**, then confirm the API base URL. It usually ends in `/v1`.
2. Add an API key if the service requires bearer authentication. Additional authentication or routing
headers are available in the collapsed advanced section.
3. Save the provider connection.
4. Refresh the model list and select a discovered model. Homarr displays the friendly model name but
stores and sends the provider's exact model ID. Context-window sizes use compact labels such as `1M`.
5. Enable the assistant and save the configuration.
Changing the provider or base URL immediately resets the unsaved credential/header draft, selected model, and
enabled state, and puts the model section into a **save connection first** state. Saving that destination change also
clears stored credentials for the previous destination. Changing the discovery path resets the selected model and
enabled state; save the new path before relying on its discovery results. For an unsaved provider or base-URL change,
the model controls, **Refresh models**, web-search switch, enable switch, and **Save assistant** remain unavailable.
After the connection is saved, refresh the model list and select a model; if discovery is blank or returns no models,
the manual model-ID field is the supported fallback. Only then enable the assistant and save the configuration.
The model discovery path defaults to `/models` and is appended to the API base URL. Leave the custom path blank when
the service does not expose a usable discovery endpoint. A manual model ID can still be used when discovery is
unavailable or returns no models.
API keys and additional header values are encrypted in the Homarr database. They are decrypted only on
the server for discovery and model requests, and they are never returned to the browser. Changing the
provider or base URL, then saving the connection, clears existing credentials. This prevents credentials for one
service from being sent to another endpoint. Additional headers are write-only; saving them replaces the complete
stored header set. Use the
explicit clear/remove flow when you want to remove previously saved headers.
When a key is already stored, the page shows its protected state instead of an ambiguous empty password
field. Removing the API key requires explicit confirmation. Save that removal before entering and saving a
different key.
:::info Container networking
For a containerized Homarr installation, `localhost` refers to the Homarr container. Use a resolvable
container name, internal DNS name, or host gateway address to reach Ollama, LM Studio, or another local
model server.
:::
The configured endpoint must support streaming Chat Completions and structured tool calls for the full
assistant experience. Model discovery alone does not guarantee tool support, except when OpenRouter
explicitly advertises the model's `tools` capability.
OpenRouter web search is an opt-in option in the model section. It registers OpenRouter's model-callable
[`openrouter:web_search` server tool](https://openrouter.ai/docs/guides/features/server-tools/web-search)
alongside Homarr's tools, so OpenRouter performs each search without
exposing the stored credential to the browser. The model decides when live research is needed and can use the
grounded results and source links in responses or dashboard notes. Homarr limits each assistant request to
three searches with five results per search. OpenRouter charges web-search usage separately from model tokens.
Completed responses show a compact web-search activity card when OpenRouter reports search usage. OpenRouter
does not expose these server-side searches as ordinary client-executed tool calls, so Homarr shows the reported
search count and the safe title and URL from each returned `url_citation` annotation instead. Citation excerpts
stay in the provider response and are not persisted in the activity card.
The option is also available for custom endpoints that forward OpenRouter's server-tools request format. This
supports OpenRouter proxies, including the future Homarr Router provider. Leave it disabled for ordinary
OpenAI-compatible endpoints because the `openrouter:*` tool type is not part of the standard OpenAI API.
The Claude preset uses Anthropic's OpenAI SDK compatibility layer. Anthropic describes that layer as
suited to testing and comparison rather than as its full native API; unsupported OpenAI fields may be
ignored. Streaming responses and tool calls are supported, which are the capabilities Homarr Assistant
uses. Choose OpenRouter or another provider when you prefer a provider-neutral production endpoint.
## Conversations and tools
Conversations and messages are stored per user in the Homarr database. The assistant stays out of the
page layout until it is opened. Users can start, revisit, and delete conversations from the
conversation menu in the floating panel.
Messages support copy, edit-and-resend, response regeneration and branch navigation, Markdown export,
and helpful/not-helpful feedback. The conversation menu also provides a compact export action beside delete for
each conversation. Its debugging-oriented Markdown includes every stored user, assistant, reasoning, tool, error,
branch-parent, and server-metadata record while redacting secret-looking fields and embedded attachment data.
The per-response export uses the same tool-aware format instead of assistant-ui's text-only copy representation.
Selecting text in a message exposes a quote action that carries the selected passage into the next prompt.
Files returned by a compatible model can be downloaded directly from the message.
Assistant responses render GitHub-flavored Markdown, including headings, links, lists, task lists, tables,
quotes, inline code, and fenced code blocks. Line breaks are preserved, and Homarr repairs common streamed
responses where a provider returns escaped line breaks or places a bold-label list on one line.
Completed fenced code blocks use language-aware syntax highlighting; a streaming block stays plain until its
content is stable. Safe same-origin links are prefetched and use client-side navigation, so opening a returned
preview, editor, board, or other Homarr item does not unload the assistant. External links keep their normal
external-browser behavior.
Fenced `mermaid` blocks render as diagrams in the current color scheme using Mermaid's strict security mode;
invalid diagrams fall back to their source instead of breaking the response.
When Homarr's icon search returns an image URL, visual result tables render that actual icon in a compact
image column instead of substituting an emoji. Assistant-provided Markdown images accept safe relative,
HTTP, and HTTPS sources and load without forwarding the Homarr page as the referrer. Image message parts
render through assistant-ui's image primitive in a bounded responsive preview with an accessible loading state,
a filename caption when available, and an in-place retry when the source fails.
Images up to 1 MB and text, Markdown, CSV, HTML, XML, or JSON documents up to 350 KB can be attached by
using the paperclip, dropping files onto the composer, or pasting an image with `Cmd/Ctrl + V`. Attachment
previews stay local to the composer where they were added and show upload state, file type, and a removable image
thumbnail or document tile. A request can contain up to five attachments.
Image understanding depends on the selected model; document text is extracted locally by Homarr and sent
as part of the prompt.
Type `@` in the composer to add permission-checked context. The nested menu contains apps, integrations,
boards, widgets, and tools available to the current user. Homarr validates every referenced ID again on
the server before adding it to the model context. Selected references appear as compact, typed pills in the
composer and sent message, while preserving assistant-ui's underlying directive syntax for the model. When the
assistant cites one of those references in its own response, Homarr renders the same typed pill and uses the
stored app or integration icon when one is available. Type `/`
for common prompt shortcuts. Both menus support keyboard navigation and keep the active result visible while
moving through long lists. The selector below the composer combines searchable, provider-grouped models with
their description, context size and image-input capability, plus the thinking-level control. Model selection is
saved to the current conversation when selected. For a persisted thread, changing the model updates that thread's
preference without changing the administrator's configured default; switching threads restores each thread's saved
model, while a new thread starts from the configured default until the user changes it. Thinking mode can be left on
**Auto**, disabled, or set
from minimal through high; explicit levels are sent using the AI SDK's provider-neutral reasoning setting and
still depend on support from the chosen model and endpoint. Reasoning text is shown live when the provider streams
it through assistant-ui's grouped-parts and chain-of-thought primitives. The compact **Thinking** trace groups
adjacent reasoning with ordinary tool calls, shows each tool's status, duration and safe human-readable target,
and expands raw input, output and result previews only on request. Approval and question tools remain full-width
outside the trace. The disclosure renders Markdown, can be opened or closed without interrupting the response,
and keeps the user's collapsed state when another message starts. A compact dot matrix distinguishes an initial
thinking pass from response streaming and settles into waiting, success, or error states in the minimized activity
bar. The state always keeps its text label, and the animation is disabled when the operating system requests
reduced motion.
A circular context ring beside the model selector gives a quick glance at cumulative conversation usage without
expanding the composer toolbar. Its accessible label includes cumulative tokens and cost when available, while
its popover keeps the latest complete context reading as a new response starts and lists every request
turn with its model, provider, input, output, cached-input and reasoning tokens, context use, duration, speed,
agent steps, routing details, and cost when reported. Before usage is available, the control remains a compact
ring instead of showing a redundant missing-data label. Closing and reopening the popover reads persisted
conversation metadata instead of resetting the display to zero. A larger ring inside the popover breaks the latest
reported context into used, remaining and total capacity plus input, cached-input, output and reasoning tokens.
Each assistant response places a small provider logo in the same stable action toolbar as copy, export,
feedback, and regenerate. Hovering the logo shows the provider, exact model, timing, throughput, and cost without
adding another metadata row to the transcript. Branch navigation occupies its own adjacent group, so switching
between response versions never pushes the response actions off-screen.
OpenRouter responses request inline usage and routing metadata. Opening detailed turn data also loads the
server-side generation record for its model steps and briefly polls while OpenRouter finishes processing it.
The details separate Homarr's end-to-end timing from OpenRouter's provider latency, generation time, and provider
throughput, and can include normalized and native token counts, moderation latency, cache discount, fallback count,
service tier, data region, BYOK status, routed provider, generation identifier, exact cost, and upstream inference
cost. Provider credentials remain server-side; the browser can request only generation identifiers already stored
on a conversation owned by the signed-in user and signed by the Homarr server for that user and conversation.
Successful generation lookups are cached briefly so polling and multi-step requests do not repeatedly query
OpenRouter for the same record.
When a provider omits cost but model discovery includes prompt and completion pricing, Homarr labels the
calculated value as estimated. Missing provider data is shown as not reported rather than inferred.
The assistant can use the same internal tool definitions as the [Homarr MCP server](./mcp).
Each tool call runs as the signed-in user, so normal Homarr permission checks still apply. Read-only
procedures can run during the agent loop. Mutating procedures require native approval unless automatic approvals are
enabled for the conversation. Neutral choices and questions still wait for the user. The approval appears as a large
**Approve and run** or **Deny** action;
the assistant does not require a second typed confirmation first. Tool cards expose their status, timing,
validated input, result, and error state without flooding the conversation with raw JSON by default.
Common collection and widget results are summarized inline as compact responsive cards, while the full
structured result remains available in the tool card details. Icon searches show the returned image variants,
file formats, and repositories instead of the size of the complete icon catalog. Their trace uses a readable
activity such as **Looking for “homarr” icons** instead of exposing the internal MCP procedure name.
Large tool-driven requests can use up to 32 agent steps and a larger response budget within one request, so
multi-item dashboard changes normally finish without requiring a follow-up “Continue” message. OpenRouter calls
Homarr tools sequentially to prevent routed models from interleaving or truncating parallel JSON arguments, while
every mutation still follows its native approval policy, including conversation-scoped automatic approvals. A safely
recoverable truncated icon-search object is repaired
before validation; incomplete search strings and all mutation inputs are never guessed.
Homarr also normalizes internal tool results to the model's JSON-compatible message format before continuing an
agent loop. Rich server values such as dates cannot invalidate the next model step, and an interrupted tool call
without a result is omitted when the conversation resumes instead of making the entire prompt fail.
The assistant can create a formatted dashboard note by adding a **Notebook** widget. Ask it for a note with
headings, lists, tasks, links, emphasis, tables, or images; it generates compatible rich-text content, shows
the native widget configuration form and standard mutation approval, then places the widget in the next
available dashboard position. The generated content is retained even though the Notebook content field is
normally hidden from widget settings.
Automatic placement checks every responsive layout and treats both widgets and complete container footprints
as occupied. New assistant-created items use the first free position in the main canvas instead of overlapping
existing content or being placed in a gutter.
Every assistant-created dashboard widget now passes through Homarr's native widget editor before it is added.
The form fills widget defaults, validates widget-specific options, and only offers compatible integrations that
the current user can use. Required integrations cannot be left empty, single-integration widgets cannot receive
multiple integrations, and the reviewed values are passed unchanged into the final approval-gated mutation.
Custom-widget tools let an administrator list, inspect, create, and update definitions. The custom-widget
form's **Design with AI** action sends a focused request into the current conversation, where the assistant
can propose the matching mutation and use the same approval controls. Existing stored secret values are
reported only as configured/not configured and are preserved on update unless replacements are explicitly
provided. For Custom Widget requests, Homarr lazily adds the complete installed skill, all bundled schema,
runtime and security references, and the current JSON Schema to the trusted authoring context. This avoids an
extra provider round trip for the large `customWidget_getSkill` and `customWidget_schema` results. If the model
discovers the Custom Widget workflow mid-response, Homarr promotes the same resources before its next step and
removes the now-redundant resource calls from provider history. Multiline JSX uses `templateLines` to avoid fragile
streamed JSON escaping. Custom Widget authoring tools are mandatory: the assistant validates the complete candidate,
creates a preview from that exact version, executes every returned preview query, checks the actual HTTP status and
response shape, and repeats the entire validation and preview sequence after any material JSX, request, source, option,
or binding change. It only reports a widget as validated, working, created, or placed when the corresponding tool result
proves that checkpoint. Large widgets are saved from the exact final tested preview session, so the approved create step
does not stream the complete JSX a second time. When the request already names a board, that board is carried into the
create result and placement continues without asking the user to select it again. Preview and saved-definition results
include safe Homarr paths that open in the assistant without a page reload. When placement is still a choice, the
assistant uses the structured question UI instead of ending with a prose question.
The authoring phase limits broad component-document retrieval and then moves into validation with the smallest useful
reference set. Supplied samples and successful preview responses are treated as binding contracts: the assistant is
instructed to render the requested fields, guard optional nested data, preserve every returned item, expose freshness
when timestamps are available, and provide a refresh path for recoverable empty or error states.
The shield control in the conversation header can enable automatic approvals for the current conversation; they are
off by default.
While enabled, Homarr accepts every affirmative approval step as it arrives: protected mutations are approved,
presented app details and board changes are used, and structured questions with an explicitly categorized
**Confirm** answer choose that answer. Neutral choices such as selecting a board still wait for input because
they do not have a safe affirmative default. Automatic approvals turn off when you switch conversations, and
the shared approval tracker prevents the same call from being handled twice when both a board widget and the
floating assistant are visible.
When the assistant genuinely needs missing information or a choice, it can pause with a structured
question. Each proposed answer is visibly categorized as **Confirm**, **Decline**, or **Option**, so the effect
of automatic approval is unambiguous. **Other** is shown only when the tool allows it (`allowOther` is not `false`);
selecting it opens a freeform response and is always a manual alternative, not an automatic approval choice. While a
question is unanswered, its controls stay in a dedicated area immediately
above the composer instead of requiring the user to find the tool call in the transcript. The question temporarily
uses the available conversation body, with its own scrolling when necessary, rather than squeezing a long option
list into a short dock beside part of the transcript. The answered choice then remains in the conversation history.
Submitting the answer resumes the same agent run automatically. The minimized activity bar shows the
question or approval that needs attention and opens directly back to the request. While an answer, app
form, or mutation approval is pending, the composer preserves its draft but does not replace the pending
tool call with a new message. If the provider stops before an interactive tool supplies complete arguments,
Homarr replaces the loading state with an explicit error and confirms that no change was made.
App creation uses Homarr's native app form inside the conversation. The form reuses the local icon picker,
including automatic matching and searches through the `icon.findIcons` procedure, before the final
`app.create` approval is requested. App fields are filled only after the complete tool input has arrived, so
streaming partial arguments cannot replace the proposed values with empty fields. The model is instructed not
to invent external icon CDN URLs.
Board appearance changes use a similar review flow. The assistant first reads the board's current settings,
including its complete custom stylesheet, and then opens an editable form with Custom CSS, Appearance,
Background, and General tabs. The form highlights how many fields were proposed, preserves settings the
assistant did not change, and returns only values that differ from the current board. If nothing differs, the review
cancels without creating a mutation or approval. Custom CSS can be
copied or restored before continuing, and Homarr warns when it contains an imported stylesheet or remote
resource URL. Continuing from the form opens the normal mutation approval before anything is saved.
The board-settings tool covers General, Background, Appearance, Custom CSS, and app-status settings. It does not edit
responsive layouts, board access control, or Danger zone actions; those remain outside this review scope.
Ask for changes such as “make the cards denser on my Home board,” “add a mobile CSS rule without removing
my existing styles,” or “change the board colors and disable app status checks.” The current user must have
modify permission on the selected board. The empty conversation includes an **Improve a dashboard** starter,
and `/style` prepares the same guided workflow from the composer.
Homarr also exposes a small set of browser-only actions to the assistant. These actions can navigate to
safe internal routes, refresh the current view, and open Homarr interfaces such as the command menu and media request search.
They never receive provider credentials and cannot navigate to arbitrary external URLs.
After an approved mutation completes, Homarr automatically refetches active page and dashboard data so
board items, settings, custom CSS, apps, and other visible results update without a full browser reload.
The refresh action in the conversation header performs the same safe refetch manually. It keeps the
conversation open while refreshing both client queries and server-rendered route data.
:::warning
Assistant prompts and selected tool results are sent to the configured model endpoint. Review that
service's privacy and retention terms before enabling this feature for an instance.
:::
Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 256 KiB

After

Width:  |  Height:  |  Size: 36 KiB

+23 -190
View File
@@ -11,18 +11,15 @@ tags:
import iconColorImage from "./img/icon-color.png"; import iconColorImage from "./img/icon-color.png";
import itemRadiusImage from "./img/item-radius.png"; import itemRadiusImage from "./img/item-radius.png";
import layoutSettingsImage from "./img/layout-settings.png"; import layoutLargeImage from "./img/layout-large.png";
import layoutSettingsCustomRailsImage from "./img/layout-settings-custom-rails.png"; import layoutMediumImage from "./img/layout-medium.png";
import boardViewModeImage from "./img/board-view-mode.png"; import layoutSmallImage from "./img/layout-small.png";
import boardEditModeImage from "./img/board-edit-mode.png";
import addBoardContentMenuImage from "./img/add-board-content-menu.png";
import keyboardTileEditingImage from "./img/keyboard-tile-editing.png";
Each dashboard is called a "board". Each dashboard is called a "board".
You can create as many boards as you want. You can create as many boards as you want.
Boards can be restricted to user groups. Boards can be restricted to user groups.
A user may also have a "home board" which they will see at the root path (e.g. my-homarr.com or my-homarr.com/boards). A user may also have a "home board" which they will see at the root path (e.g. my-homarr.com or my-homarr.com/board).
Other boards can be viewed by direct links (e.g. my-homarr.com/boards/smart-home) or via the management pages. Other boards can be viewed by direct links (e.g. my-homarr.com/board/smart-home) or via the management pages.
Names of boards must be unique because this will be used in the URL to load the desired board. Names of boards must be unique because this will be used in the URL to load the desired board.
@@ -39,10 +36,8 @@ Underscores and hyphen-minus are allowed.
### Column count ### Column count
The column count defines how many fixed-width cells are available in each row. Homarr has a complex drag-and-drop system that can be used from the browser.
One cell contains a `200 × 200` logical-pixel tile. The column count defines how many horizontal columns are available to drag and drop elements to.
Homarr scales the whole board uniformly instead of changing widget proportions, and always fits it to the available width without horizontal scrolling.
The main canvas and enabled rails stay at least one viewport tall, and the canvas grows automatically while you drag or resize items near its lower edge.
### Public ### Public
@@ -93,166 +88,32 @@ Homarr will fall back to the Homarr logo if nothing is set.
### Layout ### Layout
In this section you can configure the different responsive layouts. Homarr selects the layout with the highest breakpoint that is not wider than the current screen, then scales its complete fixed-size canvas uniformly. In this section you can configure the different responsive layouts. Depending on your screens width the layout will change.
<img ![](./img/layout-settings.png)
src={layoutSettingsImage}
alt="Board settings showing protected Mobile and Base responsive layouts with rail controls"
style={{ maxWidth: "100%" }}
/>
The layout cards show the representative geometry used for planning. This current example also shows a custom layout On each element the position and size of items can be changed seperately.
with both fixed rails enabled; the rails reserve columns and the remaining canvas reflows automatically. When adding a new layout the position and sizes will automatically be generated depending on the column count.
When adding a new item / dynamic section that will always be 1x1 on each layout and placed on the first available position.
<img
src={layoutSettingsCustomRailsImage}
alt="Board layout settings with a custom layout and both fixed rails enabled"
style={{ maxWidth: "100%" }}
/>
Every board has two protected layouts. Creating a board adds **Mobile** at `0px` with three columns and **Base** at
`768px` with the column count you choose. Both layouts can be edited, but neither can be removed.
Every `1 × 1` tile has a `200 × 200` logical-pixel content area.
The `24` logical-pixel gap between tiles is separate from that size.
A `2 × 1` tile therefore contains two fixed cell widths plus the gap between them.
Widgets see their logical size, while the board's visual scale makes the complete layout larger or smaller.
When fitting below 100%, Homarr keeps widget typography and control density at their normal readable size.
On larger canvases, the complete board grows together like browser zoom.
The complete canvas always fits the available width and never adds horizontal board scrolling.
- **Mobile** starts at `0px` and uses three columns on new boards.
- **Base** is the widest layout and starts at `768px` on new boards.
Mobile and Base can be renamed and their column counts and rails can be changed. You can add removable custom layouts
between their breakpoints.
Each layout stores independent positions and sizes. Moving or resizing a widget while editing Mobile does not move it in
Base or any custom layout. New custom layouts start from Base's current column count and rail widths; Homarr chooses a
free breakpoint between Mobile and Base, and you can then change the breakpoint, columns, and rails.
Older or incomplete boards are repaired automatically. A board with no layouts receives Mobile at `0px` with three
columns and Base at `768px` with ten columns. A board with one layout keeps it as Base and receives a generated Mobile
layout. With multiple layouts, the narrowest becomes Mobile, the widest becomes Base, and intermediate layouts remain
custom. Duplicate or out-of-order breakpoints are normalized without discarding saved positions.
Use **Reset from Base** on Mobile or a custom layout to replace all of its positions and sizes with a newly generated version of Base. The layout's name, breakpoint, and column count are kept.
The canvas is at least one window tall, even when it is empty.
Its left and right edges are hard drag boundaries, while its bottom edge extends automatically as a tile is dragged or resized downward.
Nested Containers keep their own resizable bounds.
New items use their widget's default size and new Containers use a useful `4 × 3` size when space allows.
#### Name #### Name
The name of the layout can be chosen freely. It is only used for management and is not displayed on the board. The name of the layout can be choosen freely. It is only used for the management and will not be visible on the board.
#### Column count #### Column count
The total number of columns in the layout. Changing it projects the saved positions and sizes into the new grid width without overlap. Amount of columns for this layout. Defines how many items can be placed in a row. When changed it will automatically adjust the position and size of the items.
#### Fixed rails
Each responsive layout can enable a fixed left rail, a fixed right rail, or both.
Set a rail width to zero to disable it for that layout.
Choose a width from one to three columns with the three-step slider.
The preview shows how much space remains for the main canvas.
Rail columns are reserved from the layout's total column count.
For example, a 12-column layout with a two-column left rail leaves ten columns for the canvas.
The combined rail widths cannot consume the final main-canvas column.
Enabling or widening a rail reflows canvas items into the remaining columns.
Disabling a rail moves its contents back into the next available canvas positions.
Rails fill the available window height and remain fixed while the page scrolls. They do not grow or scroll independently.
Apps, widgets, and Containers inside them still use the same fixed `200 × 200` cells and can be rearranged in edit mode, where each rail shows its name and boundary.
#### Breakpoint #### Breakpoint
The minimum screen width where the layout is used; a layout is eligible at its breakpoint and above. Mobile is fixed at Minimum width of the screen where the layout will be used.
`0px`. Custom breakpoints must be unique and sit between Mobile and Base. Base must remain the highest breakpoint. Once the minimum of the next layout is reached the layout will change.
When the initial viewport is not available, Homarr uses its remembered viewport information, client hints, or User-Agent
fallback to select a layout.
#### View and edit modes
View mode renders a static board without executing the drag-and-drop editor.
After the board is interactive, Homarr prepares the editor in the background for users who can modify it.
Entering edit mode keeps the static board visible until that preparation finishes, then enables collision-aware movement and resizing.
Collapsed Containers are expanded while editing so their contents and controls are reachable.
<img
src={boardViewModeImage}
alt="A Homarr board in view mode with the dashboard canvas and static widgets visible"
style={{ maxWidth: "100%" }}
/>
Widget content is inert in edit mode, so dragging from the inert widget area moves the tile instead of activating the widget;
settings and other controls remain usable in the compact header above the tile.
A single compatible collision can swap two items when both resulting placements fit, including different-sized or lateral moves.
For other collisions, or when a tile grows into occupied space, nearby tiles are compacted out of the way and collision chains reflow.
The visible corner controls provide a 44-pixel resize target, and edge controls appear on hover for horizontal or vertical resizing.
Resize handles stop at the tile's minimum allowed size while the pointer is still moving.
Use the pencil button in the board header to enter edit mode; click it again to save. If you leave the page with unsaved
changes, Homarr asks whether to discard them. If the editor cannot load, the board remains in view mode.
<img
src={boardEditModeImage}
alt="Board in edit mode with the board header controls and editable dashboard tiles visible"
style={{ maxWidth: "100%" }}
/>
While edit mode is active, **Add board content** creates a new item, app, integration, or Container on the main canvas.
New content uses the first available position; clicking an empty cell does not place it there, and items cannot be added
directly into a rail or Container from this menu.
<img
src={addBoardContentMenuImage}
alt="Board edit mode add-content menu showing New item, Add an app, New integration, and New container"
style={{ maxWidth: "100%" }}
/>
Pointer users can drag from the inert widget area; interactive controls are excluded. Containers use the grab button at their top-left corner.
Widgets can be resized from any edge or corner handle. Activate a Container's chrome with its top-left grip before using
its resize handles.
On a touch screen, press and hold a tile for about `200 ms` before moving it; a quick tap does not start a drag. Resize
handles support direct touch input.
Keyboard users can focus a tile, press Enter or Space to start editing, use the arrow keys to move it, and use Shift plus the arrow keys to resize it.
Press Enter, Space, or Escape to stop keyboard editing.
Keyboard moves and resizes apply immediately within the current canvas, rail, or Container; they cannot move an item
between grids. Use pointer drag or **Move / resize item** for that.
Escape stops keyboard editing without undoing those changes.
During a pointer or touch interaction, Escape cancels the move or resize and restores the layout from before that interaction.
Invalid, outside, or otherwise forbidden drops are cancelled in the same way.
The **Move / resize item** action in the edit menu remains available for precise numeric input.
<img src={keyboardTileEditingImage} alt="A focused board tile in keyboard editing mode" style={{ maxWidth: "100%" }} />
#### Preview #### Preview
Layout Settings shows a lightweight structural preview for every layout, including saved spans, Containers, widget labels, and app icons. <img src={layoutSmallImage} height={256} />
It uses a representative viewport width to project the saved geometry; it does not execute widget queries or represent every <img src={layoutMediumImage} height={256} />
pixel of the screen range where the layout is active. <img src={layoutLargeImage} height={256} />
Choose **Edit layout** to open the real board editor at that representative width on desktop. Save returns to Layout Settings. Cancel discards the local changes. On phones and tablets, the normal board editor continues to use the layout selected by the device width.
### Containers and rails
Containers can hold apps, widgets, and nested Containers.
The same Container model works on the main canvas, inside a fixed rail, or inside another Container.
Move and resize Containers directly in edit mode; nested content stays within the Container's local bounds.
Resize outlines follow the pointer continuously while a dashed preview marks the grid size that will be applied on release.
Each responsive layout stores its own Container position and size.
Each Container can configure:
- Its label and whether that label is visible.
- Whether users can collapse and expand it like an accordion.
- Whether its menu shows **Open all in new tabs** for the app links it contains.
- Its border color and custom CSS classes.
Collapsing a Container reduces its full-width header to a half-row expansion bar and compacts the surrounding layout without losing its expanded size.
Expanding it smoothly restores those bounds and moves following items out of the way without reloading the widgets inside it.
### Background ### Background
@@ -298,7 +159,7 @@ Change the color of your bookmark / app icons.
#### Item radius #### Item radius
Change the radius of your items and Containers. Change the radius of your items / categories.
<img src={itemRadiusImage} height={100} /> <img src={itemRadiusImage} height={100} />
@@ -307,11 +168,6 @@ Change the radius of your items and Containers.
This is the field where you can enter custom CSS. This is the field where you can enter custom CSS.
The styling will only be applied on the respective board page. The styling will only be applied on the respective board page.
When Homarr Assistant is enabled, users with modify permission can also ask it to edit the board's General, Background,
Appearance, Custom CSS, and app-status settings. The assistant reads the current stylesheet first and presents an
editable review form before requesting approval to save the change. It does not edit responsive layouts, access control,
or Danger zone actions.
[See this guide on how to work with custom CSS](/docs/advanced/styling/). [See this guide on how to work with custom CSS](/docs/advanced/styling/).
### Behaviour ### Behaviour
@@ -320,20 +176,6 @@ or Danger zone actions.
This will disable the status check for apps. This will disable the status check for apps.
#### Work directly from the board
Widgets expose their routine controls without requiring edit mode:
- Hold `Shift` while hovering a widget that supports advanced view to expand it in place while the other widgets
remain visible but dimmed. Release `Shift` to close it. Focus that widget and press `Shift` + `Enter` to keep the view
open until you close it. On touch devices, open it from the widget menu. Other widgets adapt in place instead.
- When signed in, outside edit mode, and with **Enable right click on widgets** enabled, right-click a widget to inspect
its live query status, refresh its data, change quick options, open settings, or run supported actions. App tiles do
not use this widget menu, and available controls follow your board and integration permissions.
The widget context menu follows the **Enable right click on widgets** preference in your user settings. On touch devices,
use the widget menu where your browser exposes it.
### Access control ### Access control
With the access control, you can permit a single user or a group to your board. With the access control, you can permit a single user or a group to your board.
@@ -391,8 +233,7 @@ Duplicating a board will not copy the users and groups that have access to the b
nor the integrations the user does not have access to. nor the integrations the user does not have access to.
::: :::
Duplicating a board will create a new board with the same settings, all responsive layouts, and independent positions as the original board. Duplicating a board will create a new board with the same settings and layout as the original board.
Duplicating a board that contains Custom Widgets also requires an administrator.
You can duplicate a board by clicking on the three dots in the list of boards and selecting "Duplicate board". You can duplicate a board by clicking on the three dots in the list of boards and selecting "Duplicate board".
![](./img/duplicate-board-modal.png) ![](./img/duplicate-board-modal.png)
@@ -450,13 +291,5 @@ For the global home board you can only select boards that are public.
### Mobile home board ### Mobile home board
The mobile home board controls which board opens for devices that Homarr detects as mobile from the User-Agent header. Same as the home board, you can also set a mobile home board.
This is separate from a board's **Mobile layout**, which controls the layout selected inside that board. It will be used for devices that are detected as mobile devices. (Using User-Agent header)
You can set a mobile home board for yourself in your user preferences, for a user or group through management, or for the
whole server with **Global mobile board** under `Management` > `Settings` > `Boards`.
For signed-in users, Homarr checks the user's mobile home board, then the user's desktop home board, then the mobile and
desktop defaults for the lowest-position matching group, followed by the mobile and desktop defaults for the `Everyone`
group, and finally the server-wide mobile and desktop defaults. Anonymous visitors use the server-wide mobile default and
then the server-wide desktop default. Server-wide fallback boards must be public. See the
[global board defaults](/docs/management/settings#boards) for the complete precedence and the required public-board rule.
@@ -58,6 +58,11 @@ This means you can automate the update of the certificates by simply replacing o
Every integration has its own way of obtaining certificates. Every integration has its own way of obtaining certificates.
Generally it is recommended to search for the documentation of the integration you are using. Generally it is recommended to search for the documentation of the integration you are using.
Most of the time you can find the certificate in the file system of the integration. Most of the time you can find the certificate in the file system of the integration.
The certificate file needs to be uploaded to Homarr. To first get the file on your computer you can either:
- use `scp user@host:/path/to/file ./certificate.pem`
- use `cat /path/to/file` to print the certificate. Then recreate the file on your machine.
Below you can find instructions for some of the integrations. Below you can find instructions for some of the integrations.
### Pi-hole (v6) ### Pi-hole (v6)
@@ -65,3 +70,9 @@ Below you can find instructions for some of the integrations.
Since version 6 of Pi-hole, the web interface can be accessed through HTTPS. Since version 6 of Pi-hole, the web interface can be accessed through HTTPS.
The certificate can be found in the path `/etc/pihole/tls_ca.crt`. To add the certificate, this file needs to be copied and uploaded to Homarr. The certificate can be found in the path `/etc/pihole/tls_ca.crt`. To add the certificate, this file needs to be copied and uploaded to Homarr.
You can find more details regarding SSL / TLS for Pi-hole [here](https://docs.pi-hole.net/api/tls/). You can find more details regarding SSL / TLS for Pi-hole [here](https://docs.pi-hole.net/api/tls/).
### Proxmox
By default, Proxmox VE uses a node certificate signed by the cluster. You only need to configure Homarr with the certificate of a single node.
The certificate can be found in any node on the path `/etc/pve/pve-root-ca.pem`.
You can learn more about certificate management in Proxmox [here](https://pve.proxmox.com/wiki/Certificate_Management).
@@ -17,9 +17,7 @@ Install the Agent Skill from the verified Homarr repository:
npx skills add https://github.com/homarr-labs/homarr --skill homarr-custom-widget npx skills add https://github.com/homarr-labs/homarr --skill homarr-custom-widget
``` ```
The portable skill gives concise authoring guidance. For connected agents, Homarr returns the complete `SKILL.md` and The portable skill gives concise authoring guidance. The Homarr MCP prompt gives connected agents a retrieval workflow: read the live schema, then fetch only the component documentation needed for the widget.
all bundled skill references once in one lazy tool response. The Homarr MCP prompt then gives the agent a
retrieval workflow: read the live schema and fetch only the component documentation needed for the widget.
The workbench's **Copy AI prompt** action is separate. It creates a self-contained prompt below 12,000 characters for an ordinary ChatGPT or Claude conversation. It includes the lean contract, bindings, recommended components, and two polished examples. It never tells an offline model to call MCP tools. The workbench's **Copy AI prompt** action is separate. It creates a self-contained prompt below 12,000 characters for an ordinary ChatGPT or Claude conversation. It includes the lean contract, bindings, recommended components, and two polished examples. It never tells an offline model to call MCP tools.
@@ -39,61 +37,38 @@ Repository code cannot give this attribution.
The authoring-prompt tool description contains one sentence. The prompt contains the workflow instructions and resource links. The authoring-prompt tool description contains one sentence. The prompt contains the workflow instructions and resource links.
`GET /api/custom-widgets/skill` and `customWidget_getSkill` return a self-contained structured skill payload: `GET /api/custom-widgets/skill` and the MCP tool return the concise skill discovery payload:
```ts ```ts
{ {
name: "homarr-custom-widget"; name: "homarr-custom-widget";
version: string; version: string;
skillMd: string; // Complete SKILL.md skillMd: string;
references: Record<string, string>;
skillsShUrl: string; skillsShUrl: string;
sourceUrl: string; sourceUrl: string;
installCommand: string; installCommand: string;
} }
``` ```
The `homarr://custom-widgets/skill` MCP resource returns the same `SKILL.md` and references once as bundled Markdown. The full installed component catalog stays available through `homarr://custom-widgets/components` and per-component resources. It is deliberately not embedded in the skill or clipboard prompt.
The component catalog is a compact index of names, categories, safety levels, shared prop names, policies, and example
IDs. Per-component results contain only component-specific props and a reference to shared prop documentation. Use
`customWidget_getSharedProps` once with only the shared prop names needed across the selected components. This avoids
repeating hundreds of global props in every tool result. The catalog is deliberately not embedded in the skill or
clipboard prompt; the skill response still contains the complete authoring, schema, runtime, and security references.
These interfaces do not return an archive URL or archive hash. These interfaces do not return an archive URL or archive hash.
## Create a Custom Widget with MCP ## Create a Custom Widget with MCP
1. Load `customWidget_getSkill`, then get the prompt and live schema. 1. Get the prompt, live schema, and component catalog.
2. Call `customWidget_getComponentCatalog`, followed by `customWidget_getComponent` for only the relevant installed 2. Read the external API documentation.
components and `customWidget_getExample` when one catalog example matches the interaction pattern. Fetch at most 3. Build one definition.
eight component documents before the first validation (or four after a complete example already matches). If shared 4. Validate the definition without saving it.
props are needed, collect their catalog names and call `customWidget_getSharedProps` once for that set. 5. Create a preview session.
3. Read the external API documentation. 6. Test the queries and actions.
4. Build one definition, using `templateLines` for multiline JSX tool input. 7. Open the preview URL with browser tools.
5. Validate the definition without saving it and repair every reported issue. 8. Inspect the redacted journal and revise the local candidate until it is valid.
6. Create a preview session. 9. Create the widget, including any source URLs and credentials the user already supplied.
7. Run `customWidget_previewQuery` for every query returned by the preview and inspect its actual status and data shape. Use 10. Otherwise, ask the user to configure the saved definition with the ten-minute Homarr source-setup URL.
`{}` for load queries; manual queries require exactly the returned primitive `parameterNames`. 11. Create a final preview with the saved definition ID so Homarr loads its stored credentials, then test it again.
8. Test relevant actions in simulation mode. 12. Update the saved definition if the final verification finds a problem.
9. Open the returned preview path with browser tools. 13. Repeat the procedure for the next widget.
10. Inspect the redacted journal and revise the local candidate until validation, data, and rendering all work. Keep the
exact tested preview candidate locally. Any definition, template, or source change requires a new validation and preview;
earlier query evidence belongs to the previous preview revision.
11. Create the widget from that final tested preview with `customWidget_createFromPreview`. The preview session already carries
the source URLs and encrypted credentials supplied during preview; the create call accepts only its session ID plus an
optional target board and persists that tested preview.
12. Open its returned management path, and use a structured question when the user must choose whether to place it on a board.
13. Otherwise, ask the user to configure the saved definition with the ten-minute Homarr source-setup URL.
14. Create a final preview with the saved definition ID so Homarr loads its stored credentials, then test it again.
15. Update the saved definition if the final verification finds a problem. `customWidget_update` also accepts
`templateLines` for multiline JSX without resetting omitted options.
16. Repeat the procedure for the next widget.
Successful validation returns a compact summary instead of echoing the full widget and template. Preview creation
returns its session, links, and query checklist without repeating the submitted definition. Keep the candidate locally
and reuse it for the next lifecycle step.
Each source-configuration URL applies to one definition or preview and one source. It is short-lived, single-use, and Each source-configuration URL applies to one definition or preview and one source. It is short-lived, single-use, and
can only be opened and completed by an authenticated Homarr administrator. The administrator confirms the server URL, can only be opened and completed by an authenticated Homarr administrator. The administrator confirms the server URL,
@@ -119,29 +94,7 @@ Homarr maintainers can run the same self-contained prompt that the workbench cop
OPENROUTER_API_KEY=... pnpm --filter @homarr/custom-widgets test:ai OPENROUTER_API_KEY=... pnpm --filter @homarr/custom-widgets test:ai
``` ```
The manifest suite covers nine complex and public-API prompts. Its deterministic fixture-backed cases include a fake The suite uses DeepSeek V4 Pro to author Pokédex, Portainer, football, Jellyfin, and Home Assistant widgets. Before DeepSeek V4 Flash judges a result, it must pass Homarr's real clipboard parser, canonical schema, and deterministic scenario checks for the documented API routes, authentication, bindings, actions, and essential runtime components. It saves every response, validated widget manifest, and detailed category scores under `packages/custom-widgets/.ai-evaluations/`.
homelab health API, CoinMarketCap's keyless public API, Bored, Agify, and a multi-request Pokédex. Before the configured
model judges a result, the candidate must pass Homarr's real clipboard parser, canonical schema, and deterministic
checks for documented endpoints, authentication, request triggers, query bindings, response fields, and essential
runtime components.
To exercise the real Homarr Assistant lifecycle instead of accepting a returned JSON block, run:
```sh
OPENROUTER_API_KEY=... pnpm --filter @homarr/custom-widgets test:assistant-ai
```
The assistant test exposes the same lazy skill, live schema, component catalog, targeted component references,
validation, preview-query, and create-from-preview tools as the product. It refuses to save a definition that was not
validated exactly or whose preview queries were not all tested. Multi-request fixtures return a different payload for
each documented path and require manual-query parameters. Use `--case=pokedex`, `--case=coinmarketcap-keyless`,
`--case=bored-activity`, or `--case=agify-name` for a focused run.
Both suites default generation and harsh review to OpenRouter's `~deepseek/deepseek-v4-flash-latest` route. They save
tool traces, validated manifests, raw reviews, and detailed category scores under
`packages/custom-widgets/.ai-evaluations/`. These live tests are maintainer-invoked rather than CI-required because
they consume a configured provider account and exercise a nondeterministic model. The repository's ordinary tests
cover the lifecycle enforcement, fixture routing, score calculation, and failure behavior without network access.
Each scenario stops as soon as it passes and has a hard limit of ten generation attempts. Override the models or Each scenario stops as soon as it passes and has a hard limit of ten generation attempts. Override the models or
per-scenario limit with `OPENROUTER_GENERATOR_MODEL`, `OPENROUTER_JUDGE_MODEL`, and per-scenario limit with `OPENROUTER_GENERATOR_MODEL`, `OPENROUTER_JUDGE_MODEL`, and
@@ -149,8 +102,3 @@ per-scenario limit with `OPENROUTER_GENERATOR_MODEL`, `OPENROUTER_JUDGE_MODEL`,
into the bundled-widget source deliberately. into the bundled-widget source deliberately.
The reference API check uses a real PokeAPI list/detail request and a read-only Portainer container query when `PORTAINER_BASE_URL` and `PORTAINER_API_KEY` are set. Use `pnpm --filter @homarr/custom-widgets test:reference-apis -- --pokeapi-only` for the public PokeAPI check alone. To exercise start, restart, and stop against a disposable container, also set `PORTAINER_TEST_CONTAINER_ID` and the explicit safeguard `PORTAINER_ALLOW_MUTATIONS=1`; the script restores the container's original running state. The reference API check uses a real PokeAPI list/detail request and a read-only Portainer container query when `PORTAINER_BASE_URL` and `PORTAINER_API_KEY` are set. Use `pnpm --filter @homarr/custom-widgets test:reference-apis -- --pokeapi-only` for the public PokeAPI check alone. To exercise start, restart, and stop against a disposable container, also set `PORTAINER_TEST_CONTAINER_ID` and the explicit safeguard `PORTAINER_ALLOW_MUTATIONS=1`; the script restores the container's original running state.
Use `pnpm --filter @homarr/custom-widgets test:reference-apis -- --public-ai-apis-only` to verify the live Bored and
CoinMarketCap response shapes used by the assistant fixtures. The same command also verifies Agify when
`AGIFY_API_KEY` is configured; otherwise it reports the authenticated check as skipped without weakening the
deterministic Agify fixture tests.
@@ -1,37 +1,8 @@
# Create a Custom Widget # Create a Custom Widget
import { CanonicalCustomWidgetExample } from "@site/src/components/custom-widget-code"; import { CanonicalCustomWidgetExample } from "@site/src/components/custom-widget-code";
import workbenchImage from "./img/workbench.png";
import workbenchMobileImage from "./img/workbench-mobile.png";
import workbenchValidationImage from "./img/workbench-validation.png";
The workbench exposes **General**, **API sources**, **Requests**, **Options**, **JSX**, and **Preview** sections. Advanced JSON is The workbench is visual-first: configure the default API connection, add request cards, add option rows, then write JSX. Additional sources and raw manifest JSON remain under Advanced. Code and preview sit side by side on desktop and use tabs on mobile.
available in the manifest and request/option editors. Code and preview sit side by side on desktop; on smaller screens the
**Configure**/**Preview** switch shows one pane at a time and the section navigation scrolls horizontally. Preview sizes display
as **S/M/L** (Compact/Standard/Wide), and fixtures are **Live**, **Loading**, **Empty**, or **Error**.
For multiline JSX, prefer `templateLines` in agent or MCP input and keep the final tested preview as the source of truth before saving.
<img
src={workbenchImage}
alt="Custom Widget workbench with configuration sections and a live preview"
style={{ maxWidth: "100%" }}
/>
<img
src={workbenchMobileImage}
alt="Mobile Custom Widget workbench with Configure and Preview switch and horizontally scrollable section navigation"
style={{ maxWidth: "100%" }}
/>
The workbench keeps the configuration and preview together so you can inspect validation diagnostics, the redacted request
journal, fixture data, and preview size before saving. Invalid fields stay marked in the form while the preview explains why it
needs attention.
<img
src={workbenchValidationImage}
alt="Custom Widget workbench showing a Base URL validation error with Test, preview, and Create actions disabled"
style={{ maxWidth: "100%" }}
/>
## Manual workflow ## Manual workflow
@@ -50,9 +21,7 @@ needs attention.
<CanonicalCustomWidgetExample id="widget-options" label="Widget options" example="options" height="360px" /> <CanonicalCustomWidgetExample id="widget-options" label="Widget options" example="options" height="360px" />
Every option has a default. Supported controls include text, textarea, number, switch, select, multi-select, slider, date, time, color, icon, URL, duration, time zone, and JSON. Dynamic choices use `choicesFrom: { request, itemsPath?, valuePath, labelPath }`; the request must be a load query without invocation parameters, and you cannot provide both static `choices` and `choicesFrom`. Every option has a default. Supported controls include text, textarea, number, switch, select, multi-select, slider, date, time, color, icon, URL, duration, time zone, and JSON. Dynamic choices use `choicesFrom` with a query ID and value/label paths.
Preview actions are simulated by default and recorded in the journal without contacting the service. Enable **Run actions against the configured API** only when you intend a real mutation; live actions still require configured confirmation.
## ChatGPT or Claude ## ChatGPT or Claude
@@ -62,8 +31,7 @@ The model must return one fenced `json` block containing the complete widget, in
field. Copy that single block and select **Paste**. Homarr fills the visual builders and the JSX editor field. Copy that single block and select **Paste**. Homarr fills the visual builders and the JSX editor
without any placeholder replacement or second paste. without any placeholder replacement or second paste.
Homarr shows all actionable schema or JSX errors inline. Enter credentials separately. The copied prompt does not include the Homarr shows all actionable schema or JSX errors inline. Enter credentials separately. The prompt deliberately
latest preview response; use **Preview** > **Data** to inspect it and mention important response fields in your request. The prompt deliberately
recommends a focused Mantine component set, visual hierarchy, responsive layout, theme compatibility, and loading, recommends a focused Mantine component set, visual hierarchy, responsive layout, theme compatibility, and loading,
empty, failure, and success states. empty, failure, and success states.
@@ -7,21 +7,7 @@ Templates are JSX expressions interpreted inside a widget-local safety and rende
- `options.name` — saved widget configuration - `options.name` — saved widget configuration
- `inputs.name` — temporary values from declarative `bind` - `inputs.name` — temporary values from declarative `bind`
The v2 manifest requires a `default` source and supports up to eight sources, 64 requests, and 64 options. Source IDs are
case-insensitively unique. Each request allows up to 32 static headers and 32 invalidation targets; a widget can contain
up to 24 secrets, each up to 8,192 characters, and templates are limited to 50,000 characters. Validation reports the
exact field when a limit is exceeded.
Authoring input accepts either `template` or `templateLines`, not both. `templateLines` accepts 1–2,000 lines, each up to
10,000 characters; after joining lines with newlines, the complete template must still fit the 50,000-character limit.
The safe interpreter also bounds JSX evaluation: AST depth 64, 25,000 operations, 4,000 collection items, 10,000 rendered
nodes, and 200,000 characters in one string or rendered value. API responses are bounded separately before they reach the
template.
Use normal Mantine layouts, typography, feedback, dates, and charts. Official compound names such as `Card.Section`, `Tabs.List`, and `Radio.Group` are supported. Ordinary safe props pass through; a dangerous capability produces an explicit error. Use normal Mantine layouts, typography, feedback, dates, and charts. Official compound names such as `Card.Section`, `Tabs.List`, and `Radio.Group` are supported. Ordinary safe props pass through; a dangerous capability produces an explicit error.
Unsafe URLs, callback props, and unapproved style properties are stripped. Response data is deep-sanitized and budgeted
before it reaches `data`, so treat it as untrusted input even when the source is private.
## Temporary inputs ## Temporary inputs
@@ -29,7 +15,7 @@ before it reaches `data`, so treat it as untrusted input even when the source is
<Stack> <Stack>
<TextInput bind="search" defaultValue="" label="Search" /> <TextInput bind="search" defaultValue="" label="Search" />
<SubFetch requestId="search" params={{ query: inputs.search }}> <SubFetch requestId="search" params={{ query: inputs.search }}>
{(results, meta) => ( {(results) => (
<Stack> <Stack>
{(results ?? []).map((item) => ( {(results ?? []).map((item) => (
<Text key={item.id}>{item.name}</Text> <Text key={item.id}>{item.name}</Text>
@@ -46,23 +32,14 @@ Bound values live only in React memory while the widget is mounted. They are not
Use expressions, conditionals, optional chaining, arrays, objects, template strings, safe standard helpers, and expression callbacks for `map`, `filter`, `sort`, and trusted runtime slots. Callback blocks, authored `const`, IIFEs, authored recursion, arbitrary functions, imports, hooks, refs, raw events, browser requests, `eval`, and bigint are blocked. Use expressions, conditionals, optional chaining, arrays, objects, template strings, safe standard helpers, and expression callbacks for `map`, `filter`, `sort`, and trusted runtime slots. Callback blocks, authored `const`, IIFEs, authored recursion, arbitrary functions, imports, hooks, refs, raw events, browser requests, `eval`, and bigint are blocked.
Bounded regex literals are supported for string matching, splitting, search, and replacement. Regex literals are limited to Bounded regex literals are supported for string matching, splitting, search, and replacement. Patterns with lookbehind, backreferences, nested quantifiers, excessive length, or unsupported flags are rejected.
128 characters and flags `gimsu`; lookarounds, backreferences, repeated alternatives/groups, unsupported flags, and more
than one variable quantifier are rejected.
## Runtime components ## Runtime components
- `SubFetch` runs a named query. Automatic queries run when the widget loads; set `trigger="manual"` to defer one and pass - `SubFetch` runs a manual query with its `params`. Set `triggerContent` to a card or image to make that content the accessible click and keyboard trigger instead of showing the default load button.
primitive `params` for its invocation references. With `triggerContent`, any content node (commonly a card or image) becomes - `ActionButton` and `ToggleSwitch` run actions.
the accessible click and keyboard trigger instead of showing the default load button. - `RefreshButton` renders a compact, accessible icon control that refreshes load queries for authenticated viewers. It
- `SubFetch` with `trigger="manual"` keeps its result local to that instance; it does not populate the shared is disabled for anonymous public-board viewers because refreshing invalidates shared server cache state.
`data.requestId` or `status.requestId` roots. The child callback receives `(result, meta)`, while loading, errors, and
retry are handled by `SubFetch`. Provide both `triggerContent` and `triggerAriaLabel` when custom content launches it.
- `ActionButton` and `ToggleSwitch` run actions only when the widget is mounted outside edit mode. A toggle needs both
enabled and disabled request parameters and rolls back its visual state if the action fails.
- `RefreshButton` renders a compact, accessible icon control that invalidates the client&apos;s load-query cache so active load
queries can refetch; it does not execute a request itself. It is disabled when the widget has no active item or preview
session, or while the board is in edit mode.
- `Icon`/`TablerIcon` resolves installed safe icons. - `Icon`/`TablerIcon` resolves installed safe icons.
Always include responsive loading, empty, error, and success states. Runtime failures remain inside the widget tile. Always include responsive loading, empty, error, and success states. Runtime failures remain inside the widget tile.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

@@ -40,17 +40,6 @@ interface HomarrCustomWidgetV2 {
} }
``` ```
### Design with AI
The **Design with AI** action is available while creating or editing a custom widget. Describe what the widget should show and,
when [Homarr Assistant](../assistant) is enabled, send the request directly from the form. The generated prompt includes the
widget schema and current non-secret configuration, but not the latest preview response. It proposes the matching create or
update tool for your approval.
Use the preview's **Data** tab to inspect the endpoint's real response shape and mention important fields in your request; the
copied prompt does not automatically include the latest preview response. Saved secrets are never added to the prompt. **Copy to
clipboard** remains available when the Assistant is disabled or when you prefer another AI service.
<CustomWidgetCodeExample <CustomWidgetCodeExample
id="complete-widget-manifest" id="complete-widget-manifest"
label="Complete manifest" label="Complete manifest"
@@ -67,9 +56,9 @@ preview, publish, install, configure, or write secrets, including through HTTP a
only lists Custom Widget MCP tools, prompts, and resources when its owner is an administrator. only lists Custom Widget MCP tools, prompts, and resources when its owner is an administrator.
Adding or configuring a Custom Widget placement requires both administrator access and permission to modify that Adding or configuring a Custom Widget placement requires both administrator access and permission to modify that
board. Duplicating a board that contains Custom Widgets also requires an administrator. A non-administrator with board. A non-administrator with board-modify access can move, resize, change presentation settings, or remove an
board-modify access can move, resize, change presentation settings, or remove an existing placement, but cannot add one, existing placement, but cannot add one, change its definition, refresh interval, or Custom Widget options. Duplicating
change its definition, refresh interval, or Custom Widget options. a board that contains Custom Widgets also requires an administrator.
Once an administrator has placed a widget, ordinary board permissions apply to rendering and requests. Authorized Once an administrator has placed a widget, ordinary board permissions apply to rendering and requests. Authorized
non-administrators can use it on private boards, and queries can render for anonymous viewers on public boards. non-administrators can use it on private boards, and queries can render for anonymous viewers on public boards.
@@ -104,8 +93,10 @@ logs, or exports.
For administrators adding widgets while editing a board, the Custom Widgets section includes **Import from For administrators adding widgets while editing a board, the Custom Widgets section includes **Import from
Workshop**. Non-administrators do not see the Custom Widget picker or Workshop actions. Selecting **Install widget** Workshop**. Non-administrators do not see the Custom Widget picker or Workshop actions. Selecting **Install widget**
opens a two-step modal flow: first review the Workshop entry and its complete source in the read-only editor, then opens a two-step modal flow: first review the Workshop entry and its complete source in the read-only editor, then
confirm API sources and credentials. After import, the new definition appears in the picker. Its tile waits for the board confirm API sources and credentials. The steps keep the same dimensions and the previous step remains available when
to be saved before server requests run. you go back. After import, the new definition appears in the picker. Its tile
waits for the board to be saved before server requests run and loads automatically after a successful save; no page
reload is required.
## Import and Workshop ## Import and Workshop
@@ -134,7 +125,6 @@ replace the v1 backup.
## Bundled examples ## Bundled examples
Homarr seeds five disabled examples without credentials and never overwrites an edited seed. They use the same public Homarr seeds five disabled examples without credentials and never overwrites an edited seed. They use the same public contract. The Tautulli example is the highest-scoring widget preserved from Homarr's AI authoring evaluation:
contract. Tautulli Activity is one of the bundled examples preserved from Homarr's AI authoring evaluation:
<BundledCustomWidgetGallery /> <BundledCustomWidgetGallery />
@@ -1,11 +1,6 @@
# Configure requests and security # Configure requests and security
Most widgets use a single source named `default`; the manifest must contain it. Extra keyed sources are available for widgets Most widgets use a single source named `default`. Extra keyed sources are available under Advanced for widgets that combine APIs. Authentication is `none`, `bearer`, `basic`, an API-key header, or an API-key query parameter. Credentials belong to the widget but remain outside its manifest.
that combine APIs. Source IDs are case-insensitively unique. Base URLs must use HTTP(S), contain no embedded credentials,
query string, or fragment, and sources can be `public`, `private`, or `loopback`. Authentication is `none`, `bearer`,
`basic`, an API-key header, or an API-key query parameter. Credentials belong to the widget but remain outside its manifest.
Source origins are fixed by the definition and source-setup flow; widget options and invocation parameters cannot redirect a
request to a different origin.
## Named requests ## Named requests
@@ -40,28 +35,19 @@ Bind values directly where they are used:
`{option:name}` and `$option` read saved widget options. `{param:name}` and `$param` read invocation-time `params`. Load queries cannot use invocation parameters. Homarr infers the exact parameter names and primitive values from these references, rejecting missing or extra invocation params at the request boundary. `{option:name}` and `$option` read saved widget options. `{param:name}` and `$param` read invocation-time `params`. Load queries cannot use invocation parameters. Homarr infers the exact parameter names and primitive values from these references, rejecting missing or extra invocation params at the request boundary.
Path placeholders and query values must resolve to text, numbers, or booleans. `$option` and `$param` references may appear Paths and query-string values must resolve to text, numbers, or booleans. JSON bodies can also use structured `json` and `multiSelect` option values, for example `{ "entity_ids": { "$option": "entities" } }`.
recursively in JSON bodies; invocation parameters remain primitive, and load queries cannot use `$param`. JSON bodies can
also use structured `json` and `multiSelect` option values, for example `{ "entity_ids": { "$option": "entities" } }`.
## Protection ## Protection
Every request uses Homarr's existing server executor: same-origin source enforcement, network scope, DNS and redirect validation, SSRF protection, board permissions, rate and request limits, body and response limits, timeouts, and encrypted credential injection. Static headers cannot override credentials, cookies, routing, proxy, forwarding, or hop-by-hop headers. Every request uses Homarr's existing server executor: same-origin source enforcement, network scope, DNS and redirect validation, SSRF protection, board permissions, rate and request limits, body and response limits, timeouts, and encrypted credential injection. Static headers cannot override credentials, cookies, routing, proxy, forwarding, or hop-by-hop headers.
The executor caps request bodies at 10 KiB and response bodies at 1 MiB. Parsed JSON is limited to depth 32 and 50,000 nodes;
each network hop has a ten-second timeout, queries follow at most three same-origin redirects, actions do not follow redirects,
and the total request deadline is 45 seconds.
The limiter allows four concurrent requests per user/item and eight per definition. Per minute, a user/item can make up to 60
queries, 10 actions, or 3 DELETE actions; a definition can receive up to 240, 40, or 12 of those request categories.
Queries default to view permission and actions to modify. DELETE always requires full permission and confirmation. Actions are never cached, retried, or run on load. Queries default to view permission and actions to modify. DELETE always requires full permission and confirmation. Actions are never cached, retried, or run on load.
Management and runtime are intentionally separate. A Homarr administrator with board-modify permission places and Management and runtime are intentionally separate. A Homarr administrator with board-modify permission places and
configures the widget. After placement, query and action authorization follows the request's board permission. Public configures the widget. After placement, query and action authorization follows the request's board permission. Public
boards may execute queries for anonymous viewers, but mutations stay authenticated. This includes shared-cache boards may execute queries for anonymous viewers, but mutations stay authenticated. This includes shared-cache
invalidation: `RefreshButton` is disabled only without an active item or preview session, or in edit mode, while ordinary invalidation: `RefreshButton` is disabled for anonymous viewers, while ordinary scheduled query rendering remains
scheduled query rendering remains available to anonymous viewers. available.
Responses use bounded, versioned shared-Redis cache keys. Action invalidation advances the shared version so every Responses use bounded, versioned shared-Redis cache keys. Action invalidation advances the shared version so every
Homarr replica stops serving the previous response without scanning or deleting a key prefix. Homarr replica stops serving the previous response without scanning or deleting a key prefix.
@@ -41,8 +41,7 @@ The workbench marks the invalid field while you type. An invalid URL does not re
An invalid definition disables widget preview and Save/Create. The Data, Options, Actions, and Diagnostics tabs remain available so you can correct it. An invalid definition disables widget preview and Save/Create. The Data, Options, Actions, and Diagnostics tabs remain available so you can correct it.
The Diagnostics tab shows current schema and JSX errors, accessibility checks, and (after a preview) the redacted request The Diagnostics tab shows the current schema errors.
journal. It does not expose credentials or raw secret values.
## Workshop unavailable ## Workshop unavailable
Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

+26 -65
View File
@@ -8,24 +8,15 @@ tags:
# Model Context Protocol (MCP) # Model Context Protocol (MCP)
Homarr exposes a [Model Context Protocol](https://modelcontextprotocol.io) server at `/api/mcp`, enabling AI assistants like Claude, Cursor, VS Code Copilot, and Claude Code to interact with your Homarr instance using natural language. Homarr exposes a [Model Context Protocol](https://modelcontextprotocol.io) server at `/api/mcp/mcp`, enabling AI assistants like Claude, Cursor, VS Code Copilot, and Claude Code to interact with your Homarr instance using natural language.
The endpoint implements the stateless MCP 2026-07-28 protocol and accepts 2025 Streamable HTTP clients through a
stateless compatibility path. It does not create protocol sessions or require sticky sessions. Homarr creates the
server and extracts its tools only after an authenticated request reaches the route. The previous `/api/mcp/mcp` URL
remains available as a compatibility alias, but new clients should always use `/api/mcp`.
With MCP, you can ask your AI assistant to manage apps, integrations, users, boards, Docker containers, media servers, DNS settings, and more - all through Homarr's existing tRPC API, now exposed as 50+ AI-callable tools. With MCP, you can ask your AI assistant to manage apps, integrations, users, boards, Docker containers, media servers, DNS settings, and more - all through Homarr's existing tRPC API, now exposed as 50+ AI-callable tools.
The optional [Homarr Assistant](./assistant) uses these same tool definitions internally.
Unlike external MCP clients, it uses the current Homarr session directly and asks for in-conversation
approval before running mutating procedures.
## Getting Started ## Getting Started
The MCP endpoint lives under the **API** page at `Management` → `Tools` → `API` → **AI / MCP** tab. The MCP endpoint lives under the **API** page at `Management` → `Tools` → `API` → **MCP** tab.
![AI / MCP tab showing the endpoint, API-key warning, and client configuration](./api/img/mcp-instructions.png) ![MCP Instructions Page](./api/img/mcp-instructions.png)
### 1. Create an API Key ### 1. Create an API Key
@@ -51,7 +42,7 @@ Add the following to your client's MCP configuration file:
{ {
"mcpServers": { "mcpServers": {
"homarr": { "homarr": {
"url": "https://your-homarr-instance.com/api/mcp", "url": "https://your-homarr-instance.com/api/mcp/mcp",
"headers": { "headers": {
"ApiKey": "<your-api-key>" "ApiKey": "<your-api-key>"
} }
@@ -82,7 +73,7 @@ Replace `<your-api-key>` with your actual API key. Never commit API keys to vers
"mcpServers": { "mcpServers": {
"homarr": { "homarr": {
"command": "npx", "command": "npx",
"args": ["-y", "mcp-remote", "https://your-homarr-instance.com/api/mcp", "--header", "ApiKey:<your-api-key>"] "args": ["-y", "mcp-remote", "https://your-homarr-instance.com/api/mcp/mcp", "--header", "ApiKey:<your-api-key>"]
} }
} }
} }
@@ -105,10 +96,6 @@ Custom Widget and Workshop authoring are different: their tools, prompts, and re
key owner is a Homarr administrator. Non-administrator keys cannot discover or invoke those capabilities. Creating an key owner is a Homarr administrator. Non-administrator keys cannot discover or invoke those capabilities. Creating an
API key never elevates the owner. API key never elevates the owner.
OAuth authorization and token requests bind issued credentials to the canonical `/api/mcp` resource. Dynamic client
registration remains available for compatibility with current MCP clients while the ecosystem transitions toward
pre-registered clients and Client ID Metadata Documents.
## Testing the Connection ## Testing the Connection
Verify your MCP endpoint is working: Verify your MCP endpoint is working:
@@ -116,17 +103,13 @@ Verify your MCP endpoint is working:
```bash ```bash
curl -s -X POST \ curl -s -X POST \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "Accept: application/json" \ -H "Accept: application/json, text/event-stream" \
-H "ApiKey: <your-api-key>" \ -H "ApiKey: <your-api-key>" \
-H "Mcp-Method: server/discover" \ -d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}' \
-H "Mcp-Name: homarr" \ https://your-homarr-instance.com/api/mcp/mcp
-d '{"jsonrpc":"2.0","method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"test","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{}}},"id":1}' \
https://your-homarr-instance.com/api/mcp
``` ```
A successful response includes the supported protocol versions and server capabilities. Use `tools/list` to see all A successful response includes the server capabilities and available tools. Use `tools/list` to see all available tools:
available tools. MCP v2 clients add the per-request metadata and `Mcp-Method` / `Mcp-Name` headers automatically; the
abbreviated request below uses the stateless 2025 compatibility path for readability:
```bash ```bash
curl -s -X POST \ curl -s -X POST \
@@ -134,22 +117,19 @@ curl -s -X POST \
-H "Accept: application/json, text/event-stream" \ -H "Accept: application/json, text/event-stream" \
-H "ApiKey: <your-api-key>" \ -H "ApiKey: <your-api-key>" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}' \ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' \
https://your-homarr-instance.com/api/mcp https://your-homarr-instance.com/api/mcp/mcp
``` ```
Tool catalogs are returned in deterministic order and may be cached privately by the client for five minutes. The
removed HTTP+SSE `/sse` and `/message` transports return `410 Gone`; use the single Streamable HTTP endpoint instead.
## Available Tools ## Available Tools
Homarr exposes 50+ tools organized by namespace. Each tool is either a **query** (GET, read-only) or **mutation** (POST, write action). Homarr exposes 50+ tools organized by namespace. Each tool is either a **query** (GET, read-only) or **mutation** (POST, write action).
| Namespace | Example Tools | Type | | Namespace | Example Tools | Type |
| ------------------ | ---------------------------------------------------------- | ---------- | | ------------------ | --------------------------------------------------------- | ---------- |
| `app` | List all apps, get app by ID, create/update/delete app | GET + POST | | `app` | List all apps, get app by ID, create/update/delete app | GET + POST |
| `apiKeys` | List API keys | GET | | `apiKeys` | List API keys | GET |
| `beszel` | Systems status, alerts, historical system metrics | GET | | `beszel` | Systems status, alerts, historical system metrics | GET |
| `board` | List boards, read settings/CSS, create/update/delete board | GET + POST | | `board` | List boards, get board by ID, create/update/delete board | GET + POST |
| `docker` | List containers, inspect container, start/stop/restart | GET + POST | | `docker` | List containers, inspect container, start/stop/restart | GET + POST |
| `icon` | Search icons | GET | | `icon` | Search icons | GET |
| `info` | Server health, version info | GET | | `info` | Server health, version info | GET |
@@ -195,36 +175,21 @@ An agent creates one `homarr-custom-widget-v2` definition at a time. Start with
The API key owner must be a Homarr administrator. The same rule applies to management, import, migration, preview, The API key owner must be a Homarr administrator. The same rule applies to management, import, migration, preview,
secrets, publication, Workshop installation, and the HTTP authoring resources. secrets, publication, Workshop installation, and the HTTP authoring resources.
Load the installed skill before authoring. `customWidget_getSkill` returns the complete `SKILL.md` entrypoint and every Then, read the live schema and component resources. These resources describe the installed Mantine components.
bundled reference in one response, so an MCP client does not need filesystem access or a second documentation fetch.
Then read the live schema and only the component resources required by the design. These resources describe the
installed Mantine components.
1. Call `customWidget_getSkill`, `customWidget_getAuthoringPrompt`, `customWidget_schema`, and 1. Call `customWidget_getAuthoringPrompt` and `customWidget_schema`.
`customWidget_getComponentCatalog`. 2. Build one Custom Widget definition.
2. Fetch only the relevant installed component documentation with `customWidget_getComponent` and, when useful, 3. Call `customWidget_validate` without saving the definition.
one interaction example with `customWidget_getExample`. Component results contain component-specific props; collect 4. Call `customWidget_previewCreate`.
any needed shared prop names from the compact catalog and fetch them together once with `customWidget_getSharedProps`. 5. Test named requests with `customWidget_previewQuery` and `customWidget_previewAction`.
3. Build one Custom Widget definition. Use `templateLines` for generated multiline JSX to avoid a fragile JSON-escaped 6. Open the returned preview URL.
template string. 7. Examine `customWidget_previewJournal`.
4. Call `customWidget_validate` without saving the definition and repair every returned issue path. 8. Revise the local candidate and create another preview session until it is ready.
5. Call `customWidget_previewCreate`. 9. Call `customWidget_create`, supplying any credentials and deployment-specific source URLs the user already provided.
6. Call `customWidget_previewQuery` for every entry in the returned `queries` list. Supply the listed invocation 10. If the user must enter the source URL or credentials, call `customWidget_configurationRequestUser` with the new definition ID and wait for completion.
parameters for manual queries and inspect the real HTTP status and response shape before binding template fields. 11. Create a final preview with that definition ID so Homarr loads its stored credentials, then test it again.
7. Test relevant actions with `customWidget_previewAction` in simulation mode. 12. Use `customWidget_update` or `customWidget_templatePatch` for any final saved-definition corrections.
8. Open the returned `previewPath` or `previewUrl`. 13. Repeat this procedure for the next Custom Widget.
9. Examine `customWidget_previewJournal`.
10. Revise the local candidate and create another preview session until validation, query data, actions, and rendering
are ready.
11. Call `customWidget_create`, supplying any credentials and deployment-specific source URLs the user already provided.
12. Open the returned `managementPath` to review the exact saved definition.
13. If the user must enter the source URL or credentials, call `customWidget_configurationRequestUser` with the new definition ID and wait for completion.
14. Create a final preview with that definition ID so Homarr loads its stored credentials, then test it again.
15. Use `customWidget_update` or `customWidget_templatePatch` for any final saved-definition corrections.
16. Repeat this procedure for the next Custom Widget.
Validation and preview creation return compact lifecycle metadata. They do not echo the submitted widget or duplicate
its template as both a string and line array; the agent should retain and reuse its local candidate.
The agent receives the configuration-request status. The agent does not receive plaintext credentials. A preview-scoped setup URL configures only that temporary preview; use a definition-scoped URL when the values must persist. The agent receives the configuration-request status. The agent does not receive plaintext credentials. A preview-scoped setup URL configures only that temporary preview; use a definition-scoped URL when the values must persist.
@@ -260,10 +225,6 @@ The Custom Widget tools are:
- `customWidget_workshopGet` - `customWidget_workshopGet`
- `customWidget_workshopInstall` - `customWidget_workshopInstall`
- `customWidget_getAuthoringPrompt` - `customWidget_getAuthoringPrompt`
- `customWidget_getComponentCatalog`
- `customWidget_getComponent`
- `customWidget_getSharedProps`
- `customWidget_getExample`
- `customWidget_getSkill` - `customWidget_getSkill`
Custom Widget permissions are not assignable group capabilities. Administrator status is required for the complete Custom Widget permissions are not assignable group capabilities. Administrator status is required for the complete
+1 -5
View File
@@ -4,8 +4,6 @@ description: "Display and edit a selected note from Anchor"
hide_title: true hide_title: true
--- ---
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { WidgetHeader } from "@site/src/components/widgets/header"; import { WidgetHeader } from "@site/src/components/widgets/header";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration"; import { WidgetConfig } from "@site/src/components/widgets/configuration";
@@ -35,12 +33,10 @@ import anchorNoteWidgetScreenshot from "./img/widget.png";
### Interactions ### Interactions
- Hover the widget, focus within it, or use a touch device to reveal **Edit**, then select it to start editing the note. - Click **Edit** to start editing the selected note.
- Click **Save** to persist changes to Anchor. - Click **Save** to persist changes to Anchor.
- Click **Cancel** to discard unsaved changes. - Click **Cancel** to discard unsaved changes.
<WidgetInteractionGuide />
### Adding the widget ### Adding the widget
<AddingWidget /> <AddingWidget />
+1 -6
View File
@@ -13,6 +13,7 @@ import { appWidget } from ".";
The app widget allows you to add a link to an application or a website that you want to access quickly from your Homarr board. The app widget allows you to add a link to an application or a website that you want to access quickly from your Homarr board.
It also supports a status check to indicate whether Homarr can reach the application or website. It also supports a status check to indicate whether Homarr can reach the application or website.
In view mode, right-clicking uses the browser's link menu. Homarr's widget menu remains available in edit mode.
### Screenshots ### Screenshots
@@ -25,12 +26,6 @@ import piHoleApp from "./img/pi-hole-ping-icon.png";
<img src={piHoleApp} alt="app pi-hole with-status-check status-check-icon" width={128 * 1.5} height={128 * 1.5} /> <img src={piHoleApp} alt="app pi-hole with-status-check status-check-icon" width={128 * 1.5} height={128 * 1.5} />
<img src={jellyseerrApp} alt="app jellyseerr with-description-tooltip" height={128 * 1.5} /> <img src={jellyseerrApp} alt="app jellyseerr with-description-tooltip" height={128 * 1.5} />
</div> </div>
### Interactions
App tiles stay compact and open their configured destination directly. They do not use the shared widget context menu;
configure the app and its status check from the tile's edit actions.
### Adding the widget ### Adding the widget
<AddingWidget /> <AddingWidget />
@@ -1,40 +0,0 @@
---
title: "ArchiveTeam Warrior"
description: "Monitor ArchiveTeam Warrior status, projects, bandwidth, and active archival jobs."
hide_title: true
---
import { archiveTeamWarriorIntegration } from "@site/docs/integrations/archiveteam-warrior";
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 { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { archiveTeamWarriorWidget } from ".";
<WidgetHeader widget={archiveTeamWarriorWidget} categories={["Archiving", "Monitoring"]} />
Use this widget to see what your ArchiveTeam Warrior is preserving without leaving Homarr. The compact view prioritizes the
selected project and Warrior status; shorter tiles can hide secondary counts, bandwidth, or the broadcast message. Advanced
view adds the runner status, exact update time, and a scrollable list of reported work items.
### Supported integration
<WidgetIntegrations
items={[
{
integration: archiveTeamWarriorIntegration,
note: "Shows status and active archival work from one Warrior instance",
},
]}
/>
<WidgetInteractionGuide />
### Adding the widget
<AddingWidget />
### Configuration
<WidgetConfig configuration={archiveTeamWarriorWidget.configuration} />
@@ -1,19 +0,0 @@
import type { WidgetDefinition } from "@site/src/types";
import { IconArchive } from "@tabler/icons-react";
export const archiveTeamWarriorWidget: WidgetDefinition = {
icon: IconArchive,
name: "ArchiveTeam Warrior",
description: "Monitor ArchiveTeam Warrior status, projects, bandwidth, and active archival jobs.",
path: "../../widgets/archive-team-warrior",
configuration: {
items: [
{
name: "Show broadcast message",
description: "Show the message broadcast by the selected Warrior project when space allows.",
values: { type: "boolean" },
defaultValue: "yes",
},
],
},
};
@@ -1,79 +0,0 @@
---
title: "Assistant"
description: "Have a complete assistant conversation directly on a board."
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 { assistantWidget } from ".";
import assistantWidgetImage from "../../management/img/assistant-widget.png";
<WidgetHeader widget={assistantWidget} categories={["Productivity"]} />
The Assistant widget brings the same conversation available from the user menu and command menu onto a
board. Messages, conversations, tool calls, approvals, files, mentions, model controls, reasoning controls,
usage details, and response actions remain synchronized with the app-wide assistant.
The assistant must be enabled by an administrator in **Management → Assistant**. Signed-out users and users
on an instance without an enabled model see an unavailable state instead of a broken composer. Signed-out users see
**Sign in**; a configured provider without a saved model or with **Enable Assistant** off remains unavailable until
the administrator saves a working connection, selects a model, and enables the assistant.
<img
src={assistantWidgetImage}
alt="Assistant widget embedded on a dashboard with its conversation header, automatic approvals control, model selector, and composer"
style={{ maxWidth: "100%" }}
/>
The current preview capture labels the board-embedded surface: its header exposes conversation history, refresh,
automatic approvals, and new-conversation controls, while the footer exposes the model, thinking mode, and composer.
### Adding the widget
<AddingWidget />
The default widget size is ten columns by four rows and automatically fits boards with fewer columns. Controls
and labels condense as the widget becomes narrower, while the conversation remains scrollable and usable on
touch devices. When a board layout does not provide enough height for conversation controls, the widget shows
a compact **Open assistant** action instead of squeezing an unusable composer into the card. Increase its height
in that layout to keep the complete conversation inline.
### Configuration
<WidgetConfig configuration={assistantWidget.configuration} />
**Current conversation** follows whichever conversation is active in the user-menu panel, `Shift + A`, or
the command menu. This is the default and keeps every assistant entry point synchronized.
**Pinned conversation** reveals a searchable conversation selector. A pinned widget never changes the
active conversation silently: select **Open conversation** inside the widget before continuing that thread. If the pinned
thread was deleted or cannot be opened, the widget keeps the pinned selection, shows the same recovery action and a
retry state, and retries that thread when **Open conversation** is selected again.
### Conversation features
- Create, switch, rename, and delete conversations.
- Send text and supported file or image attachments by picker, drag and drop, or image paste.
- Mention permitted apps, integrations, boards, widgets, and tools with `@`.
- Use `/` shortcuts for common dashboard tasks.
- Review tool calls, approve mutations, and answer structured assistant questions inline.
- Answer pending structured questions from the dock above the composer without scrolling through the conversation.
- Enable conversation-scoped automatic approvals from the shield control to approve protected mutations, use presented form details, and choose answers categorized as affirmative without individual clicks.
- Edit user messages and copy, regenerate, branch, quote, export, or rate responses.
- Inspect syntax-highlighted Markdown, sources, generated files, tool results, streaming chain-of-thought,
cumulative conversation usage, per-turn token and context use, timing, speed, provider routing, and cost.
- Change the model and reasoning effort for the active conversation. A model choice is saved per persisted thread,
does not change the administrator's default, and is restored when that thread is selected again; a new thread
starts from the configured default.
- Refresh the current board from the conversation header. Approved mutations also refresh visible board data automatically.
The expanded assistant and board widget use the same persisted thread runtime. A response continues in the
background when the widget leaves the viewport or the expanded panel is minimized. Each visible surface keeps an
independent draft, attachments, selection, and focus, so typing in a board widget cannot move or overwrite the
floating panel composer.
Automatic approvals are off by default, reset when switching conversations, and never select a neutral choice. A
structured question shows **Other** only when the tool allows it (`allowOther` is not `false`); selecting **Other**
opens a manual freeform response even when automatic approvals are enabled.
-25
View File
@@ -1,25 +0,0 @@
import type { WidgetDefinition } from "@site/src/types";
import { IconRobot } from "@tabler/icons-react";
export const assistantWidget: WidgetDefinition = {
icon: IconRobot,
name: "Assistant",
description: "Have a complete assistant conversation directly on a board.",
path: "../../widgets/assistant",
configuration: {
items: [
{
name: "Conversation",
description: "Follow the active conversation or attach the widget to one persisted conversation.",
values: { type: "select", options: ["Current conversation", "Pinned conversation"] },
defaultValue: "Current conversation",
},
{
name: "Pinned conversation",
description: "Search for a conversation. This field is hidden when Current conversation is selected.",
values: { type: "string" },
defaultValue: "",
},
],
},
};
@@ -4,8 +4,6 @@ description: "Displays media and listening statistics from Navidrome or Audioboo
hide_title: true hide_title: true
--- ---
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { WidgetHeader } from "@site/src/components/widgets/header"; import { WidgetHeader } from "@site/src/components/widgets/header";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration"; import { WidgetConfig } from "@site/src/components/widgets/configuration";
@@ -38,8 +36,6 @@ Only options relevant to your linked integration are shown in the configuration
]} ]}
/> />
<WidgetInteractionGuide />
### Adding the widget ### Adding the widget
<AddingWidget /> <AddingWidget />
-4
View File
@@ -4,8 +4,6 @@ description: "Displays missing subtitle counts and health indicators from your B
hide_title: true hide_title: true
--- ---
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { WidgetHeader } from "@site/src/components/widgets/header"; import { WidgetHeader } from "@site/src/components/widgets/header";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration"; import { WidgetConfig } from "@site/src/components/widgets/configuration";
@@ -36,8 +34,6 @@ import widgetOverview from "./img/widget-overview.png";
]} ]}
/> />
<WidgetInteractionGuide advancedView={false} />
### Adding the widget ### Adding the widget
<AddingWidget /> <AddingWidget />
@@ -4,8 +4,6 @@ description: "View Beszel alert configurations and history with triggered/ok sta
hide_title: true hide_title: true
--- ---
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { WidgetHeader } from "@site/src/components/widgets/header"; import { WidgetHeader } from "@site/src/components/widgets/header";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration"; import { WidgetConfig } from "@site/src/components/widgets/configuration";
@@ -34,8 +32,6 @@ This widget displays your Beszel alert configurations separated by status (trigg
]} ]}
/> />
<WidgetInteractionGuide advancedView={false} />
### Adding the widget ### Adding the widget
<AddingWidget /> <AddingWidget />
@@ -8,7 +8,6 @@ import { WidgetHeader } from "@site/src/components/widgets/header";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration"; import { WidgetConfig } from "@site/src/components/widgets/configuration";
import { WidgetIntegrations } from "@site/src/components/widgets/integrations"; import { WidgetIntegrations } from "@site/src/components/widgets/integrations";
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { beszelSystemGridWidget } from "."; import { beszelSystemGridWidget } from ".";
import { beszelIntegration } from "@site/docs/integrations/beszel"; import { beszelIntegration } from "@site/docs/integrations/beszel";
@@ -35,8 +34,6 @@ Select a system card to open the complete statistics view, including system, fil
]} ]}
/> />
<WidgetInteractionGuide advancedView={false} />
### Adding the widget ### Adding the widget
<AddingWidget /> <AddingWidget />
@@ -4,8 +4,6 @@ description: "Time-series charts for CPU, memory, disk, network, and Docker cont
hide_title: true hide_title: true
--- ---
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { WidgetHeader } from "@site/src/components/widgets/header"; import { WidgetHeader } from "@site/src/components/widgets/header";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration"; import { WidgetConfig } from "@site/src/components/widgets/configuration";
@@ -17,7 +15,7 @@ import beszelSystemStats from "./img/beszel-system-stats.png";
<WidgetHeader widget={beszelSystemStatsWidget} categories={["System Monitoring", "Charts"]} /> <WidgetHeader widget={beszelSystemStatsWidget} categories={["System Monitoring", "Charts"]} />
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. Use the widget controls to switch between the realtime Live view and historical periods ranging from 1 hour to 30 days. Live view keeps a one-minute window and pauses while the browser tab is hidden. If the live stream drops, the widget now shows the connection failure state immediately and can switch back to historical data. 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. Live view keeps a one-minute window and pauses while the browser tab is hidden. If the live stream drops, the widget now shows the connection failure state immediately and can switch back to historical data.
### Screenshots ### Screenshots
@@ -34,8 +32,6 @@ This widget displays time-series charts for a selected Beszel system, including
]} ]}
/> />
<WidgetInteractionGuide advancedView={false} />
### Adding the widget ### Adding the widget
<AddingWidget /> <AddingWidget />
@@ -8,15 +8,14 @@ import { WidgetHeader } from "@site/src/components/widgets/header";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration"; import { WidgetConfig } from "@site/src/components/widgets/configuration";
import { WidgetIntegrations } from "@site/src/components/widgets/integrations"; import { WidgetIntegrations } from "@site/src/components/widgets/integrations";
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { beszelSystemTableWidget } from "."; import { beszelSystemTableWidget } from ".";
import { beszelIntegration } from "@site/docs/integrations/beszel"; import { beszelIntegration } from "@site/docs/integrations/beszel";
<WidgetHeader widget={beszelSystemTableWidget} categories={["System Monitoring"]} /> <WidgetHeader widget={beszelSystemTableWidget} 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. System and load-average status dots are displayed inline with their values. Columns can be toggled on or off, dragged into a different order, and resized from their headers. Column order and widths are saved automatically for each widget. When Beszel reports extra filesystems, the disk column adds a hover breakdown for each mount alongside the root disk. 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, dragged into a different order, and resized from their headers. Column order and widths are saved automatically for each widget. 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. Select a system row to open the complete statistics view, including system, filesystem, Docker, historical, and realtime charts.
### Interactions ### Interactions
@@ -36,8 +35,6 @@ Select a system row to open the complete statistics view.
]} ]}
/> />
<WidgetInteractionGuide advancedView={false} />
### Adding the widget ### Adding the widget
<AddingWidget /> <AddingWidget />
+3 -7
View File
@@ -4,8 +4,6 @@ description: "Displays multiple app links"
hide_title: true hide_title: true
--- ---
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration"; import { WidgetConfig } from "@site/src/components/widgets/configuration";
import { WidgetHeader } from "@site/src/components/widgets/header"; import { WidgetHeader } from "@site/src/components/widgets/header";
@@ -15,19 +13,19 @@ import { bookmarksWidget } from ".";
The bookmarks widget allows you to display multiple app links in a single widget. The bookmarks widget allows you to display multiple app links in a single widget.
You can add links to your favorite applications, websites, or any other resources you want quick access to. You can add links to your favorite applications, websites, or any other resources you want quick access to.
Its normal view adapts each configured layout to the available width and height: lower-priority hostnames are hidden before names or icons, and larger collections scroll instead of crushing cards. In view mode, right-clicking uses the browser's link menu. Homarr's widget menu remains available in edit mode.
### Screenshots ### Screenshots
import gridIconsOnly from "./img/grid-icons-only.png"; import gridIconsOnly from "./img/grid-icons-only.png";
import gridWithTitle from "./img/grid-with-title.png"; import gridWithTitle from "./img/grid-with-title.png";
import verticalListAll from "./img/vertical-list-all.png"; import verticalListAll from "./img/vertical-list-all.png";
import verticalListSimple from "./img/vertical-list-simple.png"; import vericalListSimple from "./img/vertical-list-simple.png";
<div className='flex gap-1'> <div className='flex gap-1'>
<div className='flex flex-col gap-1'> <div className='flex flex-col gap-1'>
<img src={gridIconsOnly} alt="bookmarks grid icons-only" width={128} height={128} /> <img src={gridIconsOnly} alt="bookmarks grid icons-only" width={128} height={128} />
<img src={verticalListSimple} alt="bookmarks vertical-list no-domain" width={128} height={128} /> <img src={vericalListSimple} alt="bookmarks vertical-list no-domain" width={128} height={128} />
</div> </div>
<img src={gridWithTitle} alt="bookmarks grid with-title" width={256 + 4} height={256 + 4} /> <img src={gridWithTitle} alt="bookmarks grid with-title" width={256 + 4} height={256 + 4} />
@@ -35,8 +33,6 @@ import verticalListSimple from "./img/vertical-list-simple.png";
</div> </div>
<WidgetInteractionGuide />
### Adding the widget ### Adding the widget
<AddingWidget /> <AddingWidget />
Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

+11 -13
View File
@@ -4,8 +4,6 @@ description: "Display events from your integrations in a calendar view within a
hide_title: true hide_title: true
--- ---
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { WidgetHeader } from "@site/src/components/widgets/header"; import { WidgetHeader } from "@site/src/components/widgets/header";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration"; import { WidgetConfig } from "@site/src/components/widgets/configuration";
@@ -26,17 +24,19 @@ import { icalIntegration } from "@site/docs/integrations/ical";
The calendar widget shows a month view of events from different integrations. The calendar widget shows a month view of events from different integrations.
It allows you to view calendar events or upcoming releases from your media managers. It allows you to view calendar events or upcoming releases from your media managers.
Calendar requests use normal human month numbers (`1` for January through `12` for December), including when
the same data is requested through Homarr's MCP or Assistant tools.
### Screenshots ### Screenshots
import currentView from "./img/current-view.png"; import popover from "./img/popover.png";
import advancedView from "./img/advanced-view.png"; import withIntegrations from "./img/with-integrations.png";
import withoutIntegrations from "./img/without-integrations.png";
<div className="flex flex-col gap-1"> <div className="flex gap-1">
<img src={currentView} alt="Calendar month view with marked events" width={513} height={303} /> <img src={withIntegrations} alt="calendar with-integrations big-size" width={256 + 4} height={256 + 4} />
<img src={advancedView} alt="Calendar advanced view with grouped events" width={800} height={484} /> <div className="flex flex-col gap-1">
<img src={popover} alt="calendar popover-details the-mandalorian" width={128 * 3} height={128} />
<img src={withoutIntegrations} alt="calendar without-integrations small-size" width={256} height={128} />
</div>
</div> </div>
### Supported Integrations ### Supported Integrations
@@ -70,10 +70,8 @@ import advancedView from "./img/advanced-view.png";
### Interactions ### Interactions
- Hover over a marked date to preview its events in a popover. - Hover over a marked date to preview its events in a popover.
- The advanced view replaces the month grid with a grouped event list. Use its previous and next controls to change months. - Click on a marked date to pin the popover open (it will stay visible even after moving the mouse away).
- If one selected integration is unavailable, its warning indicator remains visible while events from healthy integrations continue to load. - Click the date again or click outside to close the pinned popover.
<WidgetInteractionGuide />
### Adding the widget ### Adding the widget
-4
View File
@@ -4,8 +4,6 @@ description: "Displays the current date and time."
hide_title: true hide_title: true
--- ---
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration"; import { WidgetConfig } from "@site/src/components/widgets/configuration";
import { WidgetHeader } from "@site/src/components/widgets/header"; import { WidgetHeader } from "@site/src/components/widgets/header";
@@ -27,8 +25,6 @@ import tokyoClock from "./img/tokyo-12-hour.png";
<img src={tokyoClock} alt="clock format-12-hours timezone-tokyo" width={256} height={128} /> <img src={tokyoClock} alt="clock format-12-hours timezone-tokyo" width={256} height={128} />
</div> </div>
<WidgetInteractionGuide />
### Adding the widget ### Adding the widget
<AddingWidget /> <AddingWidget />
-4
View File
@@ -4,8 +4,6 @@ description: "Overview of your Coolify instance with servers, applications, and
hide_title: true hide_title: true
--- ---
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { WidgetHeader } from "@site/src/components/widgets/header"; import { WidgetHeader } from "@site/src/components/widgets/header";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration"; import { WidgetConfig } from "@site/src/components/widgets/configuration";
@@ -27,8 +25,6 @@ The Coolify widget displays a compact overview of your Coolify instance. It can
]} ]}
/> />
<WidgetInteractionGuide advancedView={false} />
### Adding the widget ### Adding the widget
<AddingWidget /> <AddingWidget />
+7 -9
View File
@@ -1,26 +1,24 @@
--- ---
title: "Custom API" title: "Custom API"
description: "Displays data from a custom API endpoint using configurable display types and optional JSX templates." description: "The Custom API widget shows a Custom JSX v2 definition with server-side API requests, widget options, and actions."
hide_title: true hide_title: true
--- ---
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { WidgetHeader } from "@site/src/components/widgets/header"; import { WidgetHeader } from "@site/src/components/widgets/header";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { customApiWidget } from "."; import { customApiWidget } from ".";
<WidgetHeader widget={customApiWidget} categories={["Custom", "API"]} /> <WidgetHeader widget={customApiWidget} categories={["Custom", "API"]} />
The Custom API widget shows a Custom JSX v2 definition. Create the definition in [Management → Custom Widgets](/docs/management/custom-widgets).
A definition contains API sources, named requests, a widget options schema, and one safe JSX template. A definition contains API sources, named requests, a widget options schema, and one safe JSX template.
Temporary inputs exist only while the tile is mounted. Homarr encrypts source credentials separately. Temporary inputs exist only while the tile is mounted. Homarr encrypts source credentials separately.
Homarr does not send these credentials to the browser. Homarr does not include them in an export. Homarr does not send these credentials to the browser. Homarr does not include them in an export.
<WidgetInteractionGuide advancedView={false} /> ### Add the widget
### Adding the widget
<AddingWidget /> <AddingWidget />
@@ -31,9 +29,9 @@ Select a refresh interval. The board item stores the definition ID and the valid
Credentials remain on the server. Credentials remain on the server.
:::note :::note
Custom API widgets must first be created in **Management → Custom Widgets** before they appear in the widget picker.
Action buttons require the request's minimum board permission, which is `modify` by default. Anonymous viewers cannot run Enable the definition before you use it. The definition must also be available to the board.
mutations. DELETE additionally requires full permission and confirmation.
::: :::
### Permissions and actions ### Permissions and actions
@@ -1,11 +1,9 @@
--- ---
title: "DNS Hole Controls" title: "DNS Hole Controls"
description: "Control Pi-hole, AdGuard Home, or Technitium DNS from your dashboard." description: "Control PiHole or AdGuard from your dashboard."
hide_title: true hide_title: true
--- ---
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { WidgetHeader } from "@site/src/components/widgets/header"; import { WidgetHeader } from "@site/src/components/widgets/header";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration"; import { WidgetConfig } from "@site/src/components/widgets/configuration";
@@ -20,9 +18,7 @@ import { technitiumDnsIntegration } from "@site/docs/integrations/technitium-dns
<WidgetHeader widget={dnsHoleControlsWidget} categories={["DNS Hole"]} /> <WidgetHeader widget={dnsHoleControlsWidget} categories={["DNS Hole"]} />
This widget lets permitted users enable, disable, or temporarily pause one or more DNS services. It shares the summary query This widget allows you to enable or disable your DNS Hole.
and cache with DNS Hole Summary. Cards and controls adapt to the widget&apos;s width and height. Sources that fail to load are
omitted from the returned summaries; bulk actions keep successful results visible and report the number of failures.
### Screenshots ### Screenshots
@@ -50,12 +46,9 @@ import smallSize from "./img/small.png";
### Interactions ### Interactions
- Enable or disable one DNS service by clicking its status badge. - Enable / Disable one dns-hole by clicking on the badge.
- Temporarily disable one service with the clock action. - Disable one dns-hole in x seconds by clicking on the clock icon.
- Apply an action to all available services from the top action row. Homarr keeps successful results visible and reports how many sources failed. - Do all of the above for all dns-holes by using the top button row.
- Interaction controls only appear when the current user has permission for that integration.
<WidgetInteractionGuide advancedView={false} />
### Adding the widget ### Adding the widget
@@ -4,8 +4,6 @@ description: "Displays the summary of your DNS Hole."
hide_title: true hide_title: true
--- ---
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { WidgetHeader } from "@site/src/components/widgets/header"; import { WidgetHeader } from "@site/src/components/widgets/header";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration"; import { WidgetConfig } from "@site/src/components/widgets/configuration";
@@ -17,7 +15,7 @@ import { dnsHoleSummaryWidget } from ".";
<WidgetHeader widget={dnsHoleSummaryWidget} categories={["DNS Hole"]} /> <WidgetHeader widget={dnsHoleSummaryWidget} categories={["DNS Hole"]} />
This widget combines statistics from every selected DNS service, including total queries, blocked queries, and blocklist size. Its data is cached under the same DNS query family as DNS Hole Controls, so placing both widgets on a board does not create a second integration request. When the widget has enough room, it adds a scrollable status row naming each selected source. This widget allows you to display statistics about your DNS Hole like total queries, blocked queries, and more.
### Screenshots ### Screenshots
@@ -52,8 +50,6 @@ import { technitiumDnsIntegration } from "@site/docs/integrations/technitium-dns
]} ]}
/> />
<WidgetInteractionGuide advancedView={false} />
### Adding the widget ### Adding the widget
<AddingWidget /> <AddingWidget />
Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 39 KiB

@@ -8,7 +8,6 @@ import { WidgetHeader } from "@site/src/components/widgets/header";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration"; import { WidgetConfig } from "@site/src/components/widgets/configuration";
import { WidgetIntegrations } from "@site/src/components/widgets/integrations"; import { WidgetIntegrations } from "@site/src/components/widgets/integrations";
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { IconAd } from "@tabler/icons-react"; import { IconAd } from "@tabler/icons-react";
import Tabs from "@theme/Tabs"; import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem"; import TabItem from "@theme/TabItem";
@@ -50,8 +49,6 @@ Each container row has an action menu, accessible via the three-dot icon or by r
- **Remove** - permanently remove the container after a second confirmation click - **Remove** - permanently remove the container after a second confirmation click
- **Add to Homarr** - create a bookmark app from the container - **Add to Homarr** - create a bookmark app from the container
<WidgetInteractionGuide advancedView={false} />
### Adding the widget ### Adding the widget
<AddingWidget /> <AddingWidget />
Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

+16 -63
View File
@@ -4,8 +4,6 @@ description: "Allows you to view and manage your Downloads from both Torrent and
hide_title: true hide_title: true
--- ---
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { aria2Integration } from "@site/docs/integrations/aria2"; import { aria2Integration } from "@site/docs/integrations/aria2";
import { delugeIntegration } from "@site/docs/integrations/deluge"; import { delugeIntegration } from "@site/docs/integrations/deluge";
import { nzbgetIntegration } from "@site/docs/integrations/nzbget"; import { nzbgetIntegration } from "@site/docs/integrations/nzbget";
@@ -21,79 +19,36 @@ import { downloadsWidget } from ".";
<WidgetHeader widget={downloadsWidget} categories={["Torrent client", "Usenet client", "Downloads"]} /> <WidgetHeader widget={downloadsWidget} categories={["Torrent client", "Usenet client", "Downloads"]} />
This widget displays torrent, Usenet, and miscellaneous download jobs, along with their progress and other relevant information. It supports 7 download client integrations and provides a rich, responsive table interface. This widget displays a list of torrents or usenet-articles that are currently being downloaded, along with their progress and other relevant information. It supports 7 download client integrations and provides a rich, responsive table interface.
### Screenshots ### Screenshots
import downloadsCard from "./img/screenshot.png"; import downloadsCard from "./img/screenshot.png";
import contextMenuImg from "./img/context-menu.png"; import contextMenuImg from "./img/context-menu.png";
import advancedViewImg from "./img/advanced-view.png";
import filterPanelImg from "./img/filter-panel.png";
<div> <div>
<img src={downloadsCard} alt="Downloads widget showing torrent and usenet items" width={128 * 4} /> <img src={downloadsCard} alt="Downloads widget showing torrent and usenet items" width={128 * 4} />
</div> </div>
#### Download-row context menu #### Right-click context menu
<div> <div>
<img src={contextMenuImg} alt="Downloads row context menu with pause, delete, and copy actions" width={128 * 4} /> <img src={contextMenuImg} alt="Downloads widget context menu with pause, delete, and copy actions" width={128 * 4} />
</div>
#### Advanced view
<div>
<img
src={advancedViewImg}
alt="Downloads widget in advanced view with stats, sortable columns, and expanded details"
width={128 * 6}
/>
</div>
#### Advanced filter panel
<div>
<img src={filterPanelImg} alt="Downloads advanced view with the status filter panel open" width={128 * 6} />
</div> </div>
### Features ### Features
- **Responsive compact table** — in compact mode, selected columns adapt to both widget width and height; narrow or short tiles hide speed/state columns, and the shortest tiles also hide the ETA column - **Responsive table** — columns automatically adapt to widget width, hiding less important columns at smaller sizes
- **Sortable columns** — click any sortable column header to sort the current view; the configured default controls the initial column and direction - **Sortable columns** — click any column header to sort ascending/descending
- **Column customization** — choose which columns to display, then hover a header to reveal its grab handle for reordering or drag its edge to resize it; layouts save automatically per widget - **Column customization** — choose which columns to display, then hover a header to reveal its grab handle for reordering or drag its edge to resize it; layouts save automatically per widget
- **Download-row context menu** — copy a download name or ID from any row; pause, resume, and delete actions appear when you have Interact access to that integration - **Right-click context menu** — pause, resume, delete downloads, or copy download names directly from the context menu
- **Row expansion** — click any row to see full details inline without opening a separate modal - **Row expansion** — click any row to see full details inline without opening a separate modal
- **Hover tooltips** — hover a download name to see size, speeds, ETA, ratio, and category; open tooltips close when switching between compact and advanced mode - **Hover tooltips** — hover a download name to see size, speeds, ETA, ratio, and category
- **Global stats bar** — show queue progress, downloaded/total size, transfer speeds, global torrent ratio, and per-state counts in compact mode; advanced view always includes it - **Global stats bar** — toggle a summary bar showing queue progress, total size, speeds, ratio, and per-state counts
- **Client filtering** — filter downloads by integration or status using the footer filter panel - **Client filtering** — filter downloads by integration or status using the footer filter panel
- **Global controls** — pause/resume actionable queues from the footer bar, limited to selected integrations where you have Interact access - **Global controls** — pause/resume all downloads across all clients from the footer bar
- **Status indicators** — color-coded badges show download state at a glance - **Status indicators** — color-coded badges show download state at a glance
- **Speed overview** — total download and upload speeds for the selected clients are displayed in the footer - **Speed overview** — total download and upload speeds displayed in the footer
:::note
Compact layouts use both dimensions: below 500px wide, speed columns are hidden; below 300px wide, the ETA and state
columns are hidden too. A tile shorter than 180px hides speed and state columns, and a tile shorter than 120px also
hides ETA. The footer is hidden below 96px in compact mode. Advanced view bypasses these compact size reductions,
keeps the selected column set, and always includes the stats bar and footer controls, with the filter panel available
from the footer.
The filter panel changes the visible rows. Its client/status filters also change the stats bar's progress, size, and
state counts, while transfer-speed totals summarize the selected clients and the global torrent ratio summarizes the
selected torrent clients. The configured category/label filter can optionally be applied to the global torrent ratio
through **Use category filter for global torrent ratio**; footer client/status filters do not change that ratio.
The per-item **Ratio** is sent bytes divided by the item's total size. **Global Ratio** is total uploaded bytes divided
by total downloaded bytes.
Mutation controls depend on the integration's Interact permission. Users without that permission can still view rows and
use the row menu's copy actions. `slskd` is list-only: its pause, resume, and delete operations are no-ops, so those
controls have no effect for that client even when Interact permission exposes them.
The screenshot above is a Downloads row menu, separate from the shared widget context menu. When board right-click menus
are enabled, the shared menu provides widget-level actions such as advanced view, widget options, refresh, and settings.
:::
### Supported Integrations ### Supported Integrations
@@ -125,22 +80,20 @@ are enabled, the shared menu provides widget-level actions such as advanced view
}, },
{ {
integration: slskdIntegration, integration: slskdIntegration,
note: "Soulseek client (list-only; mutation controls are no-ops)", note: "Soulseek client (read-only)",
}, },
]} ]}
/> />
### Interactions ### Interactions
- **Right-click** any download to copy its name or ID; pause, resume, and delete are available only with Interact access - **Right-click** any download to open a context menu with pause/resume, delete, and copy actions
- **Click** any row to expand it and see full download details inline - **Click** any row to expand it and see full download details inline
- **Hover** a download name for a quick stats tooltip - **Hover** a download name for a quick stats tooltip
- **Stats toggle** — show or hide the global stats bar in compact mode; advanced view always shows the stats bar - **Stats toggle** — show or hide the global stats bar from the footer
- **Pause / Resume** actionable queues using the footer controls - **Pause / Resume** all downloads using the footer controls
- **Filter** rows by integration or status using the filter button in the footer - **Filter** by integration or status using the filter button in the footer
- **Sort** by clicking sortable column headers; the default sort is configured per widget, while header changes apply to the current view - **Sort** by clicking column headers (with persistent sort direction)
<WidgetInteractionGuide />
### Adding the widget ### Adding the widget
+8 -9
View File
@@ -30,20 +30,19 @@ export const downloadsWidget: WidgetDefinition = {
{ {
name: "Columns to show", name: "Columns to show",
description: description:
"Select the columns you want to display in the widget. Compact layouts automatically hide/show selected columns based on widget width and height; advanced view keeps the selected columns visible.", "Select the columns you want to display in the widget. Columns automatically hide/show based on widget width.",
values: `List of columns: ${columnsList.join(", ")}`, values: `List of columns: ${columnsList.join(", ")}`,
defaultValue: ["name", "progress", "downSpeed", "time", "state"].join(", "), defaultValue: ["name", "progress", "downSpeed", "time", "state"].join(", "),
}, },
{ {
name: "Default sort column", name: "Default sort column",
description: description: "The column used for initial sorting when the widget loads.",
"The column used for initial sorting when the widget loads. Clicking a column header changes the current view only.",
defaultValue: "progress", defaultValue: "progress",
values: `List of columns: ${sortColumns.join(", ")}`, values: `List of columns: ${sortColumns.join(", ")}`,
}, },
{ {
name: "Invert sorting", name: "Invert sorting",
description: "This sets the initial sorting direction.", description: "This will invert the default sorting order.",
values: { type: "boolean" }, values: { type: "boolean" },
defaultValue: "no", defaultValue: "no",
}, },
@@ -67,7 +66,8 @@ export const downloadsWidget: WidgetDefinition = {
}, },
{ {
name: "Hide completed torrent under this threshold (in kiB/s)", name: "Hide completed torrent under this threshold (in kiB/s)",
description: "This will hide completed torrent entries whose upload speed is below the specified threshold.", description:
"This will hide completed torrent entries that have a download speed below the specified threshold.",
values: "Any number above 0, 0 to disable", values: "Any number above 0, 0 to disable",
defaultValue: "0", defaultValue: "0",
}, },
@@ -85,16 +85,15 @@ export const downloadsWidget: WidgetDefinition = {
defaultValue: "no", defaultValue: "no",
}, },
{ {
name: "Use category filter for global torrent ratio", name: "Use filter to calculate Ratio",
description: description: "This will use the category filter when calculating the global torrent ratio.",
"When enabled, the configured category/label filter is applied to the global torrent ratio. Footer client/status filters do not change this ratio.",
values: { type: "boolean" }, values: { type: "boolean" },
defaultValue: "yes", defaultValue: "yes",
}, },
{ {
name: "Limit items per integration", name: "Limit items per integration",
description: "This will limit the number of items shown per integration, not globally.", description: "This will limit the number of items shown per integration, not globally.",
values: "Any whole number from 1 upward", values: "Any number above 1",
defaultValue: "50", defaultValue: "50",
}, },
], ],
@@ -4,8 +4,6 @@ description: "Displays a summary of firewalls."
hide_title: true hide_title: true
--- ---
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { WidgetHeader } from "@site/src/components/widgets/header"; import { WidgetHeader } from "@site/src/components/widgets/header";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetIntegrations } from "@site/src/components/widgets/integrations"; import { WidgetIntegrations } from "@site/src/components/widgets/integrations";
@@ -30,8 +28,6 @@ import { opnsenseIntegration } from "../../integrations/opnsense";
]} ]}
/> />
<WidgetInteractionGuide />
### Adding the widget ### Adding the widget
<AddingWidget /> <AddingWidget />
@@ -4,8 +4,6 @@ description: "Displays information showing the health and status of your system(
hide_title: true hide_title: true
--- ---
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { WidgetHeader } from "@site/src/components/widgets/header"; import { WidgetHeader } from "@site/src/components/widgets/header";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration"; import { WidgetConfig } from "@site/src/components/widgets/configuration";
@@ -64,8 +62,6 @@ import { synologyIntegration } from "@site/docs/integrations/synology";
]} ]}
/> />
<WidgetInteractionGuide />
### Adding the widget ### Adding the widget
<AddingWidget /> <AddingWidget />
+7 -10
View File
@@ -4,8 +4,6 @@ description: "Embed any content from the internet. Some websites may restrict ac
hide_title: true hide_title: true
--- ---
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration"; import { WidgetConfig } from "@site/src/components/widgets/configuration";
import { WidgetHeader } from "@site/src/components/widgets/header"; import { WidgetHeader } from "@site/src/components/widgets/header";
@@ -28,8 +26,6 @@ import youtubeStream from "./img/nasa-iss-stream.png";
<img src={youtubeStream} alt="iframe youtube-iss-stream" width={128 * 3} /> <img src={youtubeStream} alt="iframe youtube-iss-stream" width={128 * 3} />
</div> </div>
<WidgetInteractionGuide advancedView={false} />
### Adding the widget ### Adding the widget
<AddingWidget /> <AddingWidget />
@@ -39,14 +35,13 @@ import youtubeStream from "./img/nasa-iss-stream.png";
Although you can integrate many websites, some may not work. Although you can integrate many websites, some may not work.
This is because the administrators of those sites disallow iframes This is because the administrators of those sites disallow iframes
for security reasons, as users could easily be tricked for security reasons, as users could easily be tricked
by embedding the original site on a malicious page. by embeding the original site on a malicious page.
Some proxies disallow iframes by default for this reason. Some proxies disallow iframes by default for this reason.
You can use https://iframetest.com to check whether the site allows iframes. You can use https://iframetest.com to check, if the site is allowing iframes.
If you manage all of your own applications, you can usually change that by configuring If you manage all of your own applications, you can usually change that by configuring
the `Content-Security-Policy` header and its `frame-ancestors` directive. The obsolete the appropriate headers (such as `X-Frame-Options` or `Content-Security-Policy`) to allow iframes.
`X-Frame-Options: ALLOW-FROM` directive is not supported by modern browsers.
For example, to allow your site to be embedded in iframes from specific domains: For example, to allow your site to be embedded in iframes from specific domains:
@@ -54,12 +49,14 @@ For example, to allow your site to be embedded in iframes from specific domains:
```apache ```apache
Header set Content-Security-Policy "frame-ancestors 'self' https://trusted-domain.com" Header set Content-Security-Policy "frame-ancestors 'self' https://trusted-domain.com"
Header set X-Frame-Options "ALLOW-FROM https://trusted-domain.com"
``` ```
**Nginx:** **Nginx:**
```nginx ```nginx
add_header Content-Security-Policy "frame-ancestors 'self' https://trusted-domain.com"; add_header Content-Security-Policy "frame-ancestors 'self' https://trusted-domain.com";
add_header X-Frame-Options "ALLOW-FROM https://trusted-domain.com";
``` ```
### Example URLs ### Example URLs
@@ -85,11 +82,11 @@ To create a transparent iframe like below you can use the following html code an
![transparent iframe](./img/transparent-iframe.png) ![transparent iframe](./img/transparent-iframe.png)
### Third-party content ### Third party content
:::warning :::warning
The content below is an overview of third-party projects. All support requests/problems must be redirected to the appropriate repository. The content below is an overview of third party projects. All support requests/problems must be redirected to the appropriate repository.
We cannot guarantee the functionality nor security of these tools. We cannot guarantee the functionality nor security of these tools.
::: :::
@@ -4,8 +4,6 @@ description: "Shows a slideshow of pictures from an Immich album or your full li
hide_title: true hide_title: true
--- ---
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { immichIntegration } from "../../integrations/immich"; import { immichIntegration } from "../../integrations/immich";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration"; import { WidgetConfig } from "@site/src/components/widgets/configuration";
@@ -31,10 +29,6 @@ import screen from "./img/screen.png";
]} ]}
/> />
<WidgetInteractionGuide />
Use the pause or resume control to pause its slideshow; touch devices keep the control available. A paused slideshow keeps its resume control visible. The advanced view displays the complete photo without cropping, groups previous, pause or resume, and next together, and includes every loaded photo in its thumbnail strip.
### Adding the widget ### Adding the widget
<AddingWidget /> <AddingWidget />
@@ -4,8 +4,6 @@ description: "Information about your Immich instance"
hide_title: true hide_title: true
--- ---
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { immichIntegration } from "../../integrations/immich"; import { immichIntegration } from "../../integrations/immich";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration"; import { WidgetConfig } from "@site/src/components/widgets/configuration";
@@ -31,10 +29,6 @@ import screen from "./img/screen.png";
]} ]}
/> />
In advanced mode, the widget also ranks the largest albums by asset count. To keep the dashboard response lightweight, it shows up to 50 albums.
<WidgetInteractionGuide />
### Adding the widget ### Adding the widget
<AddingWidget /> <AddingWidget />
@@ -4,8 +4,6 @@ description: "View the status of your indexers."
hide_title: true hide_title: true
--- ---
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration"; import { WidgetConfig } from "@site/src/components/widgets/configuration";
import { WidgetIntegrations } from "@site/src/components/widgets/integrations"; import { WidgetIntegrations } from "@site/src/components/widgets/integrations";
@@ -39,8 +37,6 @@ import { prowlarrIntegration } from "@site/docs/integrations/prowlarr";
- Test all indexers by clicking on the test button. - Test all indexers by clicking on the test button.
<WidgetInteractionGuide advancedView={false} />
### Adding the widget ### Adding the widget
<AddingWidget /> <AddingWidget />
@@ -4,8 +4,6 @@ description: "Track missing media and active downloads from Radarr and Sonarr"
hide_title: true hide_title: true
--- ---
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { WidgetHeader } from "@site/src/components/widgets/header"; import { WidgetHeader } from "@site/src/components/widgets/header";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration"; import { WidgetConfig } from "@site/src/components/widgets/configuration";
@@ -60,8 +58,6 @@ The widget adapts to its container size, so it looks good at any grid size:
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. 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.
<WidgetInteractionGuide />
### Adding the widget ### Adding the widget
<AddingWidget /> <AddingWidget />
@@ -1,11 +1,9 @@
--- ---
title: "Media releases" title: "Media releases"
description: "Display newly added media or upcoming releases from different integrations" description: "Display newly added medias or upcoming releases from different integrations"
hide_title: true hide_title: true
--- ---
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { WidgetHeader } from "@site/src/components/widgets/header"; import { WidgetHeader } from "@site/src/components/widgets/header";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration"; import { WidgetConfig } from "@site/src/components/widgets/configuration";
@@ -45,8 +43,6 @@ import poster from "./img/poster.png";
]} ]}
/> />
<WidgetInteractionGuide />
### Adding the widget ### Adding the widget
<AddingWidget /> <AddingWidget />
@@ -4,8 +4,6 @@ description: "See a list of all media requests from your integration"
hide_title: true hide_title: true
--- ---
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { WidgetHeader } from "@site/src/components/widgets/header"; import { WidgetHeader } from "@site/src/components/widgets/header";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration"; import { WidgetConfig } from "@site/src/components/widgets/configuration";
@@ -46,9 +44,6 @@ import mediaRequestList from "./img/list.png";
- Approve / decline a request by clicking on the thumbs up or down icon. - Approve / decline a request by clicking on the thumbs up or down icon.
- Hover over the widget to reveal a **search button** that opens Spotlight in media request search mode, scoped to the widget's integrations. - Hover over the widget to reveal a **search button** that opens Spotlight in media request search mode, scoped to the widget's integrations.
- A warning indicator identifies unavailable integrations without hiding requests returned by healthy integrations.
<WidgetInteractionGuide advancedView={false} />
### Adding the widget ### Adding the widget
@@ -4,8 +4,6 @@ description: "Statistics about your media requests"
hide_title: true hide_title: true
--- ---
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { WidgetHeader } from "@site/src/components/widgets/header"; import { WidgetHeader } from "@site/src/components/widgets/header";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration"; import { WidgetConfig } from "@site/src/components/widgets/configuration";
@@ -19,7 +17,7 @@ import { overseerrIntegration } from "@site/docs/integrations/overseerr";
<WidgetHeader widget={mediaRequestStatsWidget} categories={["Media requests"]} /> <WidgetHeader widget={mediaRequestStatsWidget} categories={["Media requests"]} />
Provides an overview of media requests, including totals, availability, approvals, and rejections. Resize the widget to reveal denser stat tiles; the user ranking remains scrollable instead of being truncated. Provide a statistics overview of your media requests, such as mount of requests, open requests, approvement and rejections.
### Supported Integrations ### Supported Integrations
@@ -43,9 +41,6 @@ import mediaRequestStats from "./img/stats.png";
### Interactions ### Interactions
- Hover over the widget to reveal a **search button** that opens Spotlight in media request search mode, scoped to the widget's integrations. - Hover over the widget to reveal a **search button** that opens Spotlight in media request search mode, scoped to the widget's integrations.
- A warning indicator identifies unavailable integrations while totals from healthy integrations remain visible.
<WidgetInteractionGuide advancedView={false} />
### Adding the widget ### Adding the widget
Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

+3 -14
View File
@@ -4,8 +4,6 @@ description: "Show the current streams on your media servers"
hide_title: true hide_title: true
--- ---
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { WidgetHeader } from "@site/src/components/widgets/header"; import { WidgetHeader } from "@site/src/components/widgets/header";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration"; import { WidgetConfig } from "@site/src/components/widgets/configuration";
@@ -21,22 +19,14 @@ import { plexIntegration } from "@site/docs/integrations/plex";
<WidgetHeader widget={mediaServerWidget} categories={["Media server"]} /> <WidgetHeader widget={mediaServerWidget} categories={["Media server"]} />
The media server widget shows active sessions across your media servers. The media server widget enables you to watch and view current active sessions over all your media servers.
It shows playback progress, pause state, time remaining, and whether each session is direct play, direct stream, or transcoding audio or video. Location and bitrate can be toggled independently. It also displays useful information, that you can use when providing support to users or debugging a problem.
Select a session to inspect its media metadata in an anchored popover. Advanced mode adds search, sorting, and a summary of playing and transcoding sessions while keeping separate integration instances identifiable.
If one selected server is unavailable, a warning indicator identifies the degraded result while sessions from healthy servers remain usable.
Sessions without user metadata remain visible and are labeled as an unknown user.
### Screenshots ### Screenshots
import mediaTable from "./img/table.png"; import mediaTable from "./img/table.png";
import mediaAdvancedView from "./img/advanced-view.png";
<div className="flex flex-col gap-1"> <img src={mediaTable} alt="media-server-streams" width={128 * 3 * 1.5} height={128 * 1.5} />
<img src={mediaTable} alt="Media server compact streams table" width={303} height={198} />
<img src={mediaAdvancedView} alt="Media server advanced view with stream summary" width={800} height={484} />
</div>
### Supported Integrations ### Supported Integrations
@@ -58,7 +48,6 @@ import mediaAdvancedView from "./img/advanced-view.png";
}, },
]} ]}
/> />
<WidgetInteractionGuide />
### Adding the widget ### Adding the widget
@@ -4,8 +4,6 @@ description: "Statistics, current queue and worker status of your media transcod
hide_title: true hide_title: true
--- ---
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { WidgetHeader } from "@site/src/components/widgets/header"; import { WidgetHeader } from "@site/src/components/widgets/header";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration"; import { WidgetConfig } from "@site/src/components/widgets/configuration";
@@ -43,8 +41,6 @@ import statisticsCard from "./img/statistics.png";
]} ]}
/> />
<WidgetInteractionGuide />
### Adding the widget ### Adding the widget
<AddingWidget /> <AddingWidget />
@@ -4,8 +4,6 @@ description: "Displays the status of a Minecraft server."
hide_title: true hide_title: true
--- ---
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration"; import { WidgetConfig } from "@site/src/components/widgets/configuration";
import { WidgetHeader } from "@site/src/components/widgets/header"; import { WidgetHeader } from "@site/src/components/widgets/header";
@@ -26,11 +24,9 @@ import bedrockItem from "./img/bedrock.png";
<div className="flex gap-1"> <div className="flex gap-1">
<img src={javaItem} alt="minecraft-server-status java-edition" width={128 * 1.5} height={128 * 1.5} /> <img src={javaItem} alt="minecraft-server-status java-edition" width={128 * 1.5} height={128 * 1.5} />
<img src={bedrockItem} alt="minecraft-server-status bedrock-edition" width={128 * 1.5} height={128 * 1.5} /> <img src={bedrockItem} alt="minecraft-server-status bedrock-editon" width={128 * 1.5} height={128 * 1.5} />
</div> </div>
<WidgetInteractionGuide advancedView={false} />
### Adding the widget ### Adding the widget
<AddingWidget /> <AddingWidget />
@@ -42,4 +38,4 @@ import bedrockItem from "./img/bedrock.png";
### Data source ### Data source
We use the Minecraft Server Status API to fetch the server status. We use the Minecraft Server Status API to fetch the server status.
If you need to whitelist the API, use the following URL: `https://api.mcsrvstat.us`. If you need to whitelist the api, please use the following URL: `hhttps://api.mcsrvstat.us`
@@ -4,8 +4,6 @@ description: "Display connected devices on a network"
hide_title: true hide_title: true
--- ---
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { unifiControllerIntegration } from "@site/docs/integrations/unifi-controller"; import { unifiControllerIntegration } from "@site/docs/integrations/unifi-controller";
import { AddingWidget } from "@site/src/components/widgets/adding"; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration"; import { WidgetConfig } from "@site/src/components/widgets/configuration";
@@ -38,8 +36,6 @@ import wiredCard from "./img/wired.png";
]} ]}
/> />
<WidgetInteractionGuide />
### Adding the widget ### Adding the widget
<AddingWidget /> <AddingWidget />
Loaded 100 of 944 files, more files were not shown because too many files have changed in this diff. Show more