Compare commits

...
Author SHA1 Message Date
ajnart da2e121358 docs: remove obsolete mobile sidebar references 2026-08-12 11:49:23 +02:00
ajnart c834b41d39 docs: refresh modernized widget screenshots 2026-08-12 11:40:24 +02:00
ajnart ed588d46c1 docs: finish onboarding mobile coverage 2026-08-12 01:36:53 +02:00
ajnart 8f2d28c032 docs: finish assistant coverage 2026-08-12 01:35:55 +02:00
ajnart 31d5f0801d docs: finish custom widget coverage 2026-08-12 01:34:34 +02:00
ajnart c397f69ef4 docs: finish boards dnd coverage 2026-08-12 01:33:56 +02:00
ajnart bb1cfea898 docs: finish downloads coverage 2026-08-12 01:32:46 +02:00
ajnart 2a93055068 docs: finish widget interaction coverage 2026-08-12 01:31:43 +02:00
ajnart 76d4eb6ee0 docs: deepen v2 stack coverage 2026-08-12 01:05:39 +02:00
ajnart a1feca1e1f docs: align v2 stack documentation 2026-08-11 23:27:24 +02:00
Thomas Camlong 6743ffebf1 Merge branch 'feat/repair-stack-assistant-20260811' into feat/repair-stack-release-20260811 2026-08-11 17:03:22 +02:00
Thomas Camlong 132e0e8acb Merge branch 'feat/homarr-assistant' into release/v2 2026-08-11 16:59:31 +02:00
Thomas Camlong 79d9ef8bb4 Merge branch 'feat/homarr-assistant' into release/v2 2026-08-11 11:36:31 +02:00
Thomas Camlong 3f86134881 Merge branch 'feat/homarr-assistant' into release/v2 2026-08-11 11:06:42 +02:00
Thomas Camlong 959d863ec0 Merge branch 'feat/homarr-assistant' into release/v2 2026-08-10 20:43:23 +02:00
Thomas Camlong 58bb0f5500 Merge branch 'feat/homarr-assistant' into release/v2 2026-08-10 20:13:28 +02:00
Thomas Camlong eb150b32e3 Merge branch 'feat/auto-mobile-boards' into feat/homarr-assistant 2026-08-10 20:13:28 +02:00
Thomas Camlong 38702fb66b Merge branch 'feat/widget-modernization' into feat/auto-mobile-boards 2026-08-10 20:13:28 +02:00
Thomas Camlong 593443fc72 Merge branch 'feat/dnd-kit-grid' into feat/widget-modernization 2026-08-10 20:13:27 +02:00
Thomas Camlong 057c048d01 Merge commit 'b7ddc028a' into release/v2 2026-08-10 19:48:41 +02:00
Thomas Camlong 73cb1d7d9c Merge commit '3c8fe5461' into HEAD 2026-08-10 19:17:58 +02:00
Thomas Camlong 077362a9d9 Merge commit '8557f2107' into release/v2 2026-08-10 18:49:18 +02:00
Thomas Camlong 7b996a985a Merge commit '920c1a7a4' into feat/propagate-workshop-install-release 2026-08-10 18:22:08 +02:00
Thomas Camlong e127e3de5c Merge commit '3ea931d45' into feat/propagate-workshop-admin-release 2026-08-10 17:40:58 +02:00
Thomas Camlong 2b32ece92d Merge commit '5125b46f0' into release/v2 2026-08-10 16:26:09 +02:00
Thomas Camlong a2be1826bb Merge final Workshop identity fix into release 2026-08-10 15:34:00 +02:00
Thomas Camlong 5039441e64 Merge latest assistant fixes into release 2026-08-10 15:26:13 +02:00
Thomas Camlong ae5b5a2fcb Merge docs typecheck fix into release 2026-08-10 15:23:06 +02:00
Thomas Camlong 08a9572772 Merge latest v2 stack into release 2026-08-10 15:16:17 +02:00
Thomas Camlong 1e91aed14a Merge latest assistant stack into release/v2 2026-08-10 14:25:08 +02:00
Thomas 735be2debb Merge branch 'dev' into release/v2 2026-08-10 14:20:34 +02:00
Thomas Camlong 20c032f8e8 Merge commit '6baac9298' into release/v2 2026-08-10 13:22:25 +02:00
Thomas Camlong b040fbc2d0 Merge feat/homarr-assistant into release/v2 2026-08-10 12:03:15 +02:00
Thomas Camlong 1a57581228 Merge commit 'ceec25af8' into feat/schema-release 2026-08-10 10:55:21 +02:00
Thomas Camlong 0dfc8c48c2 Merge commit '2aa8455cb' into feat/final-refresh-release 2026-08-10 10:46:17 +02:00
Thomas Camlong 0006c79673 Merge commit 'b86cfaeb7' into feat/propagate-workshop-test-release 2026-08-10 10:17:20 +02:00
Thomas Camlong 9862e886f0 Merge commit 'd1c5cfbca' into feat/refresh-release-from-assistant 2026-08-10 10:09:46 +02:00
Thomas Camlong a8df31d825 Merge remote-tracking branch 'origin/feat/homarr-assistant' into feat/stack-agent-widget-fixes-release 2026-08-10 09:40:50 +02:00
Thomas Camlong 26f141f246 Merge remote-tracking branch 'origin/feat/homarr-assistant' into feat/stack-immich-fit-release 2026-08-10 09:32:48 +02:00
Thomas Camlong 57ce8e5d99 Merge remote-tracking branch 'origin/feat/homarr-assistant' into feat/stack-downloads-tooltip-release 2026-08-10 09:27:36 +02:00
Thomas Camlong 223a20b271 Merge remote-tracking branch 'origin/feat/homarr-assistant' into feat/stack-beszel-release 2026-08-10 09:20:13 +02:00
Thomas Camlong 914b96d671 Merge v2 CI repair into release roll-up 2026-08-09 22:28:05 +02:00
Thomas Camlong 9ea04aa3c1 Merge refreshed v2 stack into release roll-up 2026-08-09 22:01:08 +02:00
Thomas Camlong 7ff1e98c3e Merge custom option test fix into v2 roll-up 2026-08-09 18:09:41 +02:00
Thomas Camlong 2c7178a475 Merge custom option test fix into assistant 2026-08-09 18:09:41 +02:00
Thomas Camlong 9453a0a0e6 Merge custom option test fix into mobile boards 2026-08-09 18:09:41 +02:00
Thomas Camlong 7e33d5a326 Merge custom option test fix into widget modernization 2026-08-09 18:09:41 +02:00
Thomas Camlong e169427360 Merge custom option test fix into dnd kit 2026-08-09 18:09:40 +02:00
Thomas Camlong f08a365fc7 Merge widget option and advanced focus fixes into v2 roll-up 2026-08-09 09:21:15 +02:00
Thomas Camlong df47b1bcf6 Merge migration renames into v2 roll-up 2026-08-09 08:15:35 +02:00
Thomas Camlong 9257e3bd7c Merge branch 'feat/v2-stack-final-runtime-assistant' into release/v2 2026-08-09 02:57:00 +02:00
Thomas Camlong cae0ff3fb7 Merge branch 'feat/v2-stack-final-runtime-assistant' into release/v2 2026-08-09 02:22:29 +02:00
Thomas Camlong 6299e85924 Merge branch feat/v2-stack-final-runtime-assistant into release/v2 2026-08-09 01:47:57 +02:00
63 changed files with 588 additions and 197 deletions

No files matched your search

@@ -6,14 +6,32 @@ tags:
Homarr has the following keyboard shortcuts:
| Shortcut | Description |
| ----------------------------------------------- | ------------------------------------------ |
| `MOD` + `K` | Opens the search bar |
| `MOD` + `J` | Toggles Dark 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` | Closes an open advanced view or board menu |
| Shortcut | Description |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `MOD` + `K` | Opens the search bar |
| `MOD` + `J` | Toggles Dark 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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 38 KiB

+2 -2
View File
@@ -85,8 +85,8 @@ To do this, enter the board settings:
![](./img/enter-board-settings.png)
Scroll down a bit and click on custom CSS.
Here you can enter your CSS code, which will be applied on the respective board page.
Scroll to the **Custom CSS** section on the board settings page. It is part of the unified settings form; enter your CSS
there and save the board settings to apply it to that board.
![](./img/custom-css-setting.png)
@@ -10,10 +10,16 @@ import imageStartFromScratchStepServerSettings from "./img/onboarding-from-scrat
import imageStartFromScratchStepAdminUser from "./img/onboarding-from-scratch-step-admin-user.png";
import imageStartFromScratchStepExternalAdminGroup from "./img/onboarding-from-scratch-step-external-admin-group.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 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 manageImportOldHomarrDropzone from "./img/old-homarr-import-dropzone.png";
import manageImportOldHomarrChooseData from "./img/old-homarr-import-choose-imported-data.png";
@@ -29,12 +35,12 @@ Congratulations on installing Homarr. Now it's time to get yourself familiar wit
## Onboarding
The onboard will show the first time you start Homarr.
The onboarding 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.
:::note
If you see this screen repeatitly after you restarted the Homarr container, your Docker mounting paths may be missing,
If you see this screen repeatedly after you restart 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.
:::
@@ -68,7 +74,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.
Here you can also disable analytics. Please note that our analytics are completly anonymous and is fully compliant with GDPR and PECR.
Here you can also disable analytics. Please note that our analytics are completely anonymous and fully compliant with GDPR and PECR.
<img
src={imageStartFromScratchStepServerSettings}
@@ -126,11 +132,12 @@ 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`.
2. Check the contents that you want to export. We recommend you to export all data.
3. Click on the export data button
4. Save the ZIP file, that is being downloaded, on your device. If you selected "integrations" or "users", you also get an encryption / decryption key.
Copy it and do not loose it!
5. Go to your new >1.0.0 Homarr instance and start the onboarding. Choose `Import from Homarr before 1.0`
4. Save the downloaded ZIP file 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.
5. Go to your new Homarr 1.0 or later 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 }} />
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 }} />
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 }} />
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 }} />
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.
@@ -147,9 +154,25 @@ Public boards are also accessible for users, that are not logged in ([see access
![](./img/manage-create-board-modal.png)
Congratulations - you've just created your first board. You can see boards, that you have access to, on this page:
Congratulations - you've just created your first board. You can see the boards you have access to on this page:
![](./img/manage-view-board.png)
<figure>
<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
@@ -173,37 +196,63 @@ 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.
To do this, edit your previously created board by clicking on the big `Open board` button:
![](./img/manage-view-board.png)
<figure>
<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:
![](./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)
- **Edit mode**: Clicking this button will toggle the edit mode. In this mode you can edit the contents of your board.
- **Edit board**: Clicking this button will toggle 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, ...).
- **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.
When entering the edit mode, you'll get some additional buttons to work with:
<img src={manageBoardHeaderEditMode} alt={"Screenshot of buttons in the edit mode"} width={200} />
<figure>
<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 item**: Click this button to add new elements to your page.
- **Exit edit mode**: Click this button to save your changes and exit edit mode.
- **Add board content**: Click this button to create a new item, app, integration, or Container.
- **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.
Outside edit mode, right-click a widget for its live status, refresh, settings, quick options, and supported actions.
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 the option "New Item":
To continue, click **New item** in the add-content menu. New content is placed at the first available position on the main
canvas; it is not inserted at a clicked empty cell or directly into a rail or Container:
<img src={manageBoardHeaderAddItem} alt={"Screenshot of the add item menu"} width={200} />
<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.
We will discuss the other options in the following sections - for now choose "App" to continue.
<img src={manageBoardHeaderChooseItem} alt={"Screenshot of choosing the app item in the modal"} width={300} />
<figure>
<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:
@@ -212,30 +261,52 @@ 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.
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.
![](./img/manage-board-edit-app.gif)
<figure>
<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:
![](./img/manage-board-choose-app.png)
<figure>
<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.
Use the resize handle at the bottom right to resize the tile.
Press and hold the handle, move the pointer, and release it when the tile has the size you want.
Resize from any visible edge or corner handle. For a Container, activate its top-left grip before using its resize handles.
Press and hold a handle, move the pointer, and release it when the tile has the size you want.
![](./img/resize-item.gif)
To move a tile in edit mode, press and hold its content, move it to the desired position, and release it.
To move a tile in edit mode, press and hold its inert content area, move it to the desired position, and release it.
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. Equally-sized tiles swap when one is dropped into the other's slot, while other neighboring tiles move out of the way automatically.
![](./img/move-item.gif)
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.
Larger tiles use multiples of that fixed unit, with a consistent gap between cells.
Homarr scales the complete board uniformly, like zooming a page.
It stops shrinking at 75% and lets you scroll horizontally instead of making text and controls too small.
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
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
Viewing a board initially uses the lightweight, non-interactive layout.
@@ -251,16 +322,17 @@ 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.
Press Escape to stop.
For precise numeric controls or touch input, choose **Move / resize item** from the tile's edit menu.
The dialog can also move the tile to another section or fixed sidebar:
Keyboard users can focus a tile, press Enter or Space, then use arrow keys to move and Shift plus arrow keys to resize
within its current canvas, rail, or Container. Press Escape to stop keyboard editing. Touch dragging starts after a short
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/manage-board-move-resize-item-menu.png)
<img src={currentKeyboardTileEditing} alt="A board tile focused for keyboard editing" style={{ maxWidth: "100%" }} />
Be sure to save your changes by exiting edit mode!
![](./img/exit-edit-mode.png)
Open **Move / resize item** from the tile menu to move the tile to another section or rail and enter precise numeric
coordinates and dimensions. After making changes, click **Save board** to persist them and leave edit mode.
## Adding your first integration
@@ -295,7 +367,14 @@ 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.
Next, enter edit mode again and add another item to your board:
![](./img/manage-board-header-add-item-menu.png)
<figure>
<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.
Click on "Add to board":
@@ -322,9 +401,7 @@ Editing a widget will trigger a data update and data will soon appear:
![](./img/widget-system-health.png)
As with every change, don't forget to save your changes using the exit edit mode button:
![](./img/exit-edit-mode.png)
As with every change, click **Save board** to persist your board changes and leave edit mode.
## Organizing items with Containers
@@ -337,7 +414,7 @@ Each Container can configure:
- 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**.
Open the add-content menu and choose **New container**.
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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 561 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

+4
View File
@@ -93,6 +93,10 @@ an optional `error` on a failed integration; media-request responses expose a `f
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.
+67 -13
View File
@@ -6,6 +6,9 @@ tags:
- 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**.
@@ -18,6 +21,13 @@ focus whenever the command menu opens, including when it is opened by an assista
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
@@ -33,6 +43,20 @@ 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
@@ -59,6 +83,15 @@ The management page presents setup as three compact sections: provider and endpo
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.
@@ -67,14 +100,24 @@ is protected, and which model is selected. Redundant active-state badges are omi
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.
The model discovery path defaults to `/models` and is appended to the API base URL. Leave it blank when
the service does not expose model discovery. A manual model ID can still be used when discovery is
disabled, unavailable, or returns no models.
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 clears existing credentials. This prevents credentials for one service from being sent
to another endpoint.
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
@@ -155,7 +198,10 @@ 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
stored on the conversation after a completed request. Thinking mode can be left on **Auto**, disabled, or set
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
@@ -195,8 +241,9 @@ calculated value as estimated. Missing provider data is shown as not reported ra
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 an explicit approval in the
conversation before they execute. The approval appears as a large **Approve and run** or **Deny** action;
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
@@ -207,7 +254,8 @@ activity such as **Looking for “homarr” icons** instead of exposing the inte
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 pauses for its normal approval. A safely recoverable truncated icon-search object is repaired
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
@@ -252,7 +300,8 @@ reference set. Supplied samples and successful preview responses are treated as
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.
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
@@ -262,8 +311,9 @@ 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. Users can select one of the proposed answers or choose **Other** and
enter their own response. While a question is unanswered, its controls stay in a dedicated area immediately
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.
@@ -282,10 +332,14 @@ 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. Custom CSS can be
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,
Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 256 KiB

+105 -35
View File
@@ -11,15 +11,18 @@ tags:
import iconColorImage from "./img/icon-color.png";
import itemRadiusImage from "./img/item-radius.png";
import layoutLargeImage from "./img/layout-large.png";
import layoutMediumImage from "./img/layout-medium.png";
import layoutSmallImage from "./img/layout-small.png";
import layoutSettingsImage from "./img/layout-settings.png";
import layoutSettingsCustomRailsImage from "./img/layout-settings-custom-rails.png";
import boardViewModeImage from "./img/board-view-mode.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".
You can create as many boards as you want.
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/board).
Other boards can be viewed by direct links (e.g. my-homarr.com/board/smart-home) or via the management pages.
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).
Other boards can be viewed by direct links (e.g. my-homarr.com/boards/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.
@@ -92,9 +95,23 @@ Homarr will fall back to the Homarr logo if nothing is set.
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.
![](./img/layout-settings.png)
<img
src={layoutSettingsImage}
alt="Board settings showing protected Mobile and Base responsive layouts with rail controls"
style={{ maxWidth: "100%" }}
/>
Every board has two protected layouts:
The layout cards show the representative geometry used for planning. This current example also shows a custom layout
with both fixed rails enabled; the rails reserve columns and the remaining canvas reflows automatically.
<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.
@@ -107,9 +124,17 @@ The complete canvas always fits the available width and never adds horizontal bo
- **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, but they cannot be removed. You can add removable custom layouts between their breakpoints.
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 are generated from Base and scaled to their selected column count.
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.
@@ -135,6 +160,7 @@ 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.
@@ -143,39 +169,73 @@ Apps, widgets, and Containers inside them still use the same fixed `200 × 200`
#### Breakpoint
The minimum screen width where the layout is used. Mobile is fixed at `0px`. Custom breakpoints must be unique and sit between Mobile and Base. Base must remain the highest breakpoint.
The minimum screen width where the layout is used; a layout is eligible at its breakpoint and above. Mobile is fixed at
`0px`. Custom breakpoints must be unique and sit between Mobile and Base. Base must remain the highest breakpoint.
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 without changing the board geometry.
Widget content is inert in edit mode, so dragging anywhere on the content moves the tile instead of activating the widget; its settings button remains visible in the compact header above the tile.
Dropping a tile into another tile's exact slot swaps their positions when both resulting placements fit without overlapping other content.
For other collisions, or when a tile grows into occupied space, nearby tiles are compacted out of the way.
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.
Exit edit mode to save the resulting positions.
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.
Pointer users can drag anywhere on a widget tile. Containers use the grab button at their top-left corner.
Resize tiles from any edge or corner handle.
On a touch screen, press and hold the tile before moving it; resize handles support direct touch input.
<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; Escape stops keyboard editing without undoing those changes.
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
Layout Settings shows a lightweight structural preview for every layout, including saved spans, Containers, widget labels, and app icons. The canvas width represents the screen range where that layout is active.
Layout Settings shows a lightweight structural preview for every layout, including saved spans, Containers, widget labels, and app icons.
It uses a representative viewport width to project the saved geometry; it does not execute widget queries or represent every
pixel of the screen range where the layout is active.
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.
<img src={layoutSmallImage} height={256} />
<img src={layoutMediumImage} height={256} />
<img src={layoutLargeImage} height={256} />
### Containers and rails
Containers can hold apps, widgets, and nested Containers.
@@ -247,9 +307,10 @@ Change the radius of your items and Containers.
This is the field where you can enter custom CSS.
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 Custom
CSS and other visual or behavior settings. The assistant reads the current stylesheet first and presents an
editable review form before requesting approval to save the change.
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/).
@@ -266,12 +327,12 @@ 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.
- Right-click a widget to inspect its live query status, refresh its data, change quick options, open settings, or run
supported actions. Available controls follow your board and integration permissions.
- If you can modify the board, right-click or touch and hold an empty grid cell to add an item there. Focus a board
section and press `Shift` + `F10` for the keyboard equivalent.
- 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.
Widget and empty-space context menus follow the **Enable right click on widgets** preference in your user settings.
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
@@ -330,7 +391,8 @@ 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.
:::
Duplicating a board will create a new board with the same settings and layout as the original board.
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 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".
![](./img/duplicate-board-modal.png)
@@ -388,5 +450,13 @@ For the global home board you can only select boards that are public.
### Mobile home board
Same as the home board, you can also set a mobile home board.
It will be used for devices that are detected as mobile devices. (Using User-Agent header)
The mobile home board controls which board opens for devices that Homarr detects as mobile from the User-Agent header.
This is separate from a board's **Mobile layout**, which controls the layout selected inside that board.
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.
@@ -67,17 +67,23 @@ These interfaces do not return an archive URL or archive hash.
1. Load `customWidget_getSkill`, then get the prompt and live schema.
2. Call `customWidget_getComponentCatalog`, followed by `customWidget_getComponent` for only the relevant installed
components and `customWidget_getExample` when one catalog example matches the interaction pattern. If shared props
are needed, collect their catalog names and call `customWidget_getSharedProps` once for that set.
components and `customWidget_getExample` when one catalog example matches the interaction pattern. Fetch at most
eight component documents before the first validation (or four after a complete example already matches). If shared
props are needed, collect their catalog names and call `customWidget_getSharedProps` once for that set.
3. Read the external API documentation.
4. Build one definition, using `templateLines` for multiline JSX tool input.
5. Validate the definition without saving it and repair every reported issue.
6. Create a preview session.
7. Run `customWidget_previewQuery` for every query returned by the preview and inspect its actual status and data shape.
7. Run `customWidget_previewQuery` for every query returned by the preview and inspect its actual status and data shape. Use
`{}` for load queries; manual queries require exactly the returned primitive `parameterNames`.
8. Test relevant actions in simulation mode.
9. Open the returned preview path with browser tools.
10. Inspect the redacted journal and revise the local candidate until validation, data, and rendering all work.
11. Create the widget, including any source URLs and credentials the user already supplied.
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.
@@ -1,8 +1,37 @@
# Create a Custom Widget
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 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.
The workbench exposes **General**, **API sources**, **Requests**, **Options**, **JSX**, and **Preview** sections. Advanced JSON is
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
@@ -21,7 +50,9 @@ The workbench is visual-first: configure the default API connection, add request
<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` with a query ID and value/label paths.
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`.
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
@@ -31,7 +62,8 @@ 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
without any placeholder replacement or second paste.
Homarr shows all actionable schema or JSX errors inline. Enter credentials separately. The prompt deliberately
Homarr shows all actionable schema or JSX errors inline. Enter credentials separately. The copied prompt does not include the
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,
empty, failure, and success states.
@@ -7,7 +7,21 @@ Templates are JSX expressions interpreted inside a widget-local safety and rende
- `options.name` — saved widget configuration
- `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.
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
@@ -15,7 +29,7 @@ Use normal Mantine layouts, typography, feedback, dates, and charts. Official co
<Stack>
<TextInput bind="search" defaultValue="" label="Search" />
<SubFetch requestId="search" params={{ query: inputs.search }}>
{(results) => (
{(results, meta) => (
<Stack>
{(results ?? []).map((item) => (
<Text key={item.id}>{item.name}</Text>
@@ -32,14 +46,23 @@ 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.
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.
Bounded regex literals are supported for string matching, splitting, search, and replacement. Regex literals are limited to
128 characters and flags `gimsu`; lookarounds, backreferences, repeated alternatives/groups, unsupported flags, and more
than one variable quantifier are rejected.
## Runtime components
- `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.
- `ActionButton` and `ToggleSwitch` run actions.
- `RefreshButton` renders a compact, accessible icon control that refreshes load queries for authenticated viewers. It
is disabled for anonymous public-board viewers because refreshing invalidates shared server cache state.
- `SubFetch` runs a named query. Automatic queries run when the widget loads; set `trigger="manual"` to defer one and pass
primitive `params` for its invocation references. With `triggerContent`, any content node (commonly a card or image) becomes
the accessible click and keyboard trigger instead of showing the default load button.
- `SubFetch` with `trigger="manual"` keeps its result local to that instance; it does not populate the shared
`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.
Always include responsive loading, empty, error, and success states. Runtime failures remain inside the widget tile.
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

@@ -42,9 +42,14 @@ 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 assistant receives the widget schema, current non-secret configuration, and latest test response, then proposes the matching create or update tool for your approval.
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.
Run **Test** first when possible so the prompt includes the endpoint's real response shape. 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.
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
id="complete-widget-manifest"
@@ -62,9 +67,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.
Adding or configuring a Custom Widget placement requires both administrator access and permission to modify that
board. A non-administrator with board-modify access can move, resize, change presentation settings, or remove an
existing placement, but cannot add one, change its definition, refresh interval, or Custom Widget options. Duplicating
a board that contains Custom Widgets also requires an administrator.
board. Duplicating a board that contains Custom Widgets also requires an administrator. A non-administrator with
board-modify access can move, resize, change presentation settings, or remove an existing placement, but cannot add one,
change its definition, refresh interval, or Custom Widget options.
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.
@@ -99,10 +104,8 @@ logs, or exports.
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**
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. The steps keep the same dimensions and the previous step remains available when
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.
confirm API sources and credentials. After import, the new definition appears in the picker. Its tile waits for the board
to be saved before server requests run.
## Import and Workshop
@@ -131,6 +134,7 @@ replace the v1 backup.
## Bundled examples
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:
Homarr seeds five disabled examples without credentials and never overwrites an edited seed. They use the same public
contract. Tautulli Activity is one of the bundled examples preserved from Homarr's AI authoring evaluation:
<BundledCustomWidgetGallery />
@@ -1,6 +1,11 @@
# Configure requests and security
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.
Most widgets use a single source named `default`; the manifest must contain it. Extra keyed sources are available for widgets
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
@@ -35,19 +40,28 @@ 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.
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" } }`.
Path placeholders and query values must resolve to text, numbers, or booleans. `$option` and `$param` references may appear
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
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.
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
boards may execute queries for anonymous viewers, but mutations stay authenticated. This includes shared-cache
invalidation: `RefreshButton` is disabled for anonymous viewers, while ordinary scheduled query rendering remains
available.
invalidation: `RefreshButton` is disabled only without an active item or preview session, or in edit mode, while ordinary
scheduled query rendering remains available to anonymous viewers.
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.
@@ -41,7 +41,8 @@ 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.
The Diagnostics tab shows the current schema errors.
The Diagnostics tab shows current schema and JSX errors, accessibility checks, and (after a preview) the redacted request
journal. It does not expose credentials or raw secret values.
## Workshop unavailable
Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

+2 -2
View File
@@ -23,9 +23,9 @@ approval before running mutating procedures.
## Getting Started
The MCP endpoint lives under the **API** page at `Management` → `Tools` → `API` → **MCP** tab.
The MCP endpoint lives under the **API** page at `Management` → `Tools` → `API` → **AI / MCP** tab.
![MCP Instructions Page](./api/img/mcp-instructions.png)
![AI / MCP tab showing the endpoint, API-key warning, and client configuration](./api/img/mcp-instructions.png)
### 1. Create an API Key
+2 -1
View File
@@ -28,7 +28,8 @@ import piHoleApp from "./img/pi-hole-ping-icon.png";
### Interactions
App tiles stay compact and open their configured destination directly. Right-click a tile to inspect its status or change its settings when the board and your preferences allow it.
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
@@ -14,7 +14,9 @@ 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 keeps the current project, runner state, job counts, bandwidth, and broadcast message visible. Advanced view adds the complete active-job list and exact update time.
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
+23 -3
View File
@@ -8,6 +8,7 @@ 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"]} />
@@ -16,7 +17,18 @@ board. Messages, conversations, tool calls, approvals, files, mentions, model co
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.
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
@@ -36,7 +48,9 @@ in that layout to keep the complete conversation inline.
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.
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
@@ -50,10 +64,16 @@ active conversation silently: select **Open conversation** inside the widget bef
- 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.
- 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.
@@ -34,7 +34,7 @@ This widget displays your Beszel alert configurations separated by status (trigg
]}
/>
<WidgetInteractionGuide />
<WidgetInteractionGuide advancedView={false} />
### Adding the widget
@@ -8,6 +8,7 @@ import { WidgetHeader } from "@site/src/components/widgets/header";
import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration";
import { WidgetIntegrations } from "@site/src/components/widgets/integrations";
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { beszelSystemGridWidget } from ".";
import { beszelIntegration } from "@site/docs/integrations/beszel";
@@ -34,6 +35,8 @@ Select a system card to open the complete statistics view, including system, fil
]}
/>
<WidgetInteractionGuide advancedView={false} />
### Adding the widget
<AddingWidget />
@@ -17,7 +17,7 @@ import beszelSystemStats from "./img/beszel-system-stats.png";
<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. Hover over or focus 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. 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.
### Screenshots
@@ -8,6 +8,7 @@ import { WidgetHeader } from "@site/src/components/widgets/header";
import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration";
import { WidgetIntegrations } from "@site/src/components/widgets/integrations";
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { beszelSystemTableWidget } from ".";
import { beszelIntegration } from "@site/docs/integrations/beszel";
@@ -35,6 +36,8 @@ Select a system row to open the complete statistics view.
]}
/>
<WidgetInteractionGuide advancedView={false} />
### Adding the widget
<AddingWidget />
Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+5 -9
View File
@@ -31,16 +31,12 @@ the same data is requested through Homarr's MCP or Assistant tools.
### Screenshots
import popover from "./img/popover.png";
import withIntegrations from "./img/with-integrations.png";
import withoutIntegrations from "./img/without-integrations.png";
import currentView from "./img/current-view.png";
import advancedView from "./img/advanced-view.png";
<div className="flex gap-1">
<img src={withIntegrations} alt="calendar with-integrations big-size" width={256 + 4} height={256 + 4} />
<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 className="flex flex-col gap-1">
<img src={currentView} alt="Calendar month view with marked events" width={513} height={303} />
<img src={advancedView} alt="Calendar advanced view with grouped events" width={800} height={484} />
</div>
### Supported Integrations
+2 -1
View File
@@ -32,7 +32,8 @@ Credentials remain on the server.
:::note
Custom API widgets must first be created in **Management → Custom Widgets** before they appear in the widget picker.
Action buttons can only be executed by administrators.
Action buttons require the request's minimum board permission, which is `modify` by default. Anonymous viewers cannot run
mutations. DELETE additionally requires full permission and confirmation.
:::
### Permissions and actions
@@ -20,7 +20,9 @@ import { technitiumDnsIntegration } from "@site/docs/integrations/technitium-dns
<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 and cache with DNS Hole Summary. Every selected source stays available in the normal scrollable view, while cards and controls adapt to the widget&apos;s width and height.
This widget lets permitted users enable, disable, or temporarily pause one or more DNS services. It shares the summary query
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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 31 KiB

@@ -8,6 +8,7 @@ import { WidgetHeader } from "@site/src/components/widgets/header";
import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration";
import { WidgetIntegrations } from "@site/src/components/widgets/integrations";
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { IconAd } from "@tabler/icons-react";
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
@@ -49,6 +50,8 @@ 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
- **Add to Homarr** - create a bookmark app from the container
<WidgetInteractionGuide advancedView={false} />
### Adding the widget
<AddingWidget />
Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

+58 -15
View File
@@ -21,36 +21,79 @@ import { downloadsWidget } from ".";
<WidgetHeader widget={downloadsWidget} categories={["Torrent client", "Usenet client", "Downloads"]} />
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.
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.
### Screenshots
import downloadsCard from "./img/screenshot.png";
import contextMenuImg from "./img/context-menu.png";
import advancedViewImg from "./img/advanced-view.png";
import filterPanelImg from "./img/filter-panel.png";
<div>
<img src={downloadsCard} alt="Downloads widget showing torrent and usenet items" width={128 * 4} />
</div>
#### Right-click context menu
#### Download-row context menu
<div>
<img src={contextMenuImg} alt="Downloads widget context menu with pause, delete, and copy actions" width={128 * 4} />
<img src={contextMenuImg} alt="Downloads row 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>
### Features
- **Responsive table** — columns automatically adapt to widget width, hiding less important columns at smaller sizes
- **Sortable columns** — click any column header to sort ascending/descending
- **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
- **Sortable columns** — click any sortable column header to sort the current view; the configured default controls the initial column and direction
- **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
- **Right-click context menu** — pause, resume, delete downloads, or copy download names directly from the context menu
- **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
- **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
- **Global stats bar** — toggle a summary bar showing queue progress, total size, speeds, ratio, and per-state counts
- **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
- **Client filtering** — filter downloads by integration or status using the footer filter panel
- **Global controls** — pause/resume all downloads across all clients from the footer bar
- **Global controls** — pause/resume actionable queues from the footer bar, limited to selected integrations where you have Interact access
- **Status indicators** — color-coded badges show download state at a glance
- **Speed overview** — total download and upload speeds displayed in the footer
- **Speed overview** — total download and upload speeds for the selected clients are 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
@@ -82,20 +125,20 @@ import contextMenuImg from "./img/context-menu.png";
},
{
integration: slskdIntegration,
note: "Soulseek client (read-only)",
note: "Soulseek client (list-only; mutation controls are no-ops)",
},
]}
/>
### Interactions
- **Right-click** any download to open a context menu with pause/resume, delete, and copy actions
- **Right-click** any download to copy its name or ID; pause, resume, and delete are available only with Interact access
- **Click** any row to expand it and see full download details inline
- **Hover** a download name for a quick stats tooltip
- **Stats toggle** — show or hide the global stats bar from the footer
- **Pause / Resume** all downloads using the footer controls
- **Filter** by integration or status using the filter button in the footer
- **Sort** by clicking column headers (with persistent sort direction)
- **Stats toggle** — show or hide the global stats bar in compact mode; advanced view always shows the stats bar
- **Pause / Resume** actionable queues using the footer controls
- **Filter** rows 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
<WidgetInteractionGuide />
+9 -8
View File
@@ -30,19 +30,20 @@ export const downloadsWidget: WidgetDefinition = {
{
name: "Columns to show",
description:
"Select the columns you want to display in the widget. Columns automatically hide/show based on widget width.",
"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.",
values: `List of columns: ${columnsList.join(", ")}`,
defaultValue: ["name", "progress", "downSpeed", "time", "state"].join(", "),
},
{
name: "Default sort column",
description: "The column used for initial sorting when the widget loads.",
description:
"The column used for initial sorting when the widget loads. Clicking a column header changes the current view only.",
defaultValue: "progress",
values: `List of columns: ${sortColumns.join(", ")}`,
},
{
name: "Invert sorting",
description: "This will invert the default sorting order.",
description: "This sets the initial sorting direction.",
values: { type: "boolean" },
defaultValue: "no",
},
@@ -66,8 +67,7 @@ export const downloadsWidget: WidgetDefinition = {
},
{
name: "Hide completed torrent under this threshold (in kiB/s)",
description:
"This will hide completed torrent entries that have a download speed below the specified threshold.",
description: "This will hide completed torrent entries whose upload speed is below the specified threshold.",
values: "Any number above 0, 0 to disable",
defaultValue: "0",
},
@@ -85,15 +85,16 @@ export const downloadsWidget: WidgetDefinition = {
defaultValue: "no",
},
{
name: "Use filter to calculate Ratio",
description: "This will use the category filter when calculating the global torrent ratio.",
name: "Use category filter for global torrent ratio",
description:
"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" },
defaultValue: "yes",
},
{
name: "Limit items per integration",
description: "This will limit the number of items shown per integration, not globally.",
values: "Any number above 1",
values: "Any whole number from 1 upward",
defaultValue: "50",
},
],
@@ -33,7 +33,7 @@ import screen from "./img/screen.png";
<WidgetInteractionGuide />
Hover or focus the widget 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.
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
Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 22 KiB

@@ -31,8 +31,12 @@ Sessions without user metadata remain visible and are labeled as an unknown user
### Screenshots
import mediaTable from "./img/table.png";
import mediaAdvancedView from "./img/advanced-view.png";
<img src={mediaTable} alt="media-server-streams" width={128 * 3 * 1.5} height={128 * 1.5} />
<div className="flex flex-col gap-1">
<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
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

+7 -4
View File
@@ -15,17 +15,20 @@ import { weatherWidget } from ".";
The Weather Widget uses your location to display the current, highest and lowest temperature.
In the compact view, select a condition icon or forecast day to open details with a mouse, keyboard or touch.
The advanced view always combines current conditions with the full seven-day forecast, including humidity, wind,
gusts, sunrise and sunset. Both views reuse the same cached Open Meteo response.
The compact forecast shows up to the configured number of days. The advanced view combines current conditions with the
available daily forecast, including humidity, wind, gusts, sunrise and sunset. Both views reuse the same cached Open
Meteo response.
### Screenshots
import forecastWeather from "./img/forecast.png";
import currentWeather from "./img/current.png";
import advancedWeather from "./img/advanced-view.png";
<div className="flex flex-col gap-1">
<img src={forecastWeather} alt="weather forecast-5-days" width={128 * 4.5} />
<img src={currentWeather} alt="weather current" width={128 * 3} />
<img src={forecastWeather} alt="Weather compact forecast" width={238} height={112} />
<img src={currentWeather} alt="Weather compact current conditions" width={198} height={93} />
<img src={advancedWeather} alt="Weather advanced view with seven-day forecast details" width={800} height={484} />
</div>
<WidgetInteractionGuide />
@@ -8,6 +8,7 @@ import { WidgetHeader } from "@site/src/components/widgets/header";
import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration";
import { WidgetIntegrations } from "@site/src/components/widgets/integrations";
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
import { wudWidget } from ".";
import { wudIntegration } from "@site/docs/integrations/whats-up-docker";
@@ -26,6 +27,8 @@ This widget shows two stats from your WUD instance: the total number of monitore
]}
/>
<WidgetInteractionGuide advancedView={false} />
### Adding the widget
<AddingWidget />
@@ -1,10 +1,10 @@
import { IconArrowsMaximize, IconLayoutGrid, IconMouse2, IconPlus } from "@tabler/icons-react";
import { IconArrowsMaximize, IconLayoutGrid, IconMouse2, IconRefresh } from "@tabler/icons-react";
const advancedInteraction = {
icon: IconArrowsMaximize,
title: "Advanced view",
description:
"Hold Shift while hovering for an in-place preview, press Shift + Enter, or choose Open advanced view from the widget menu.",
"Supported widgets explicitly opt into advanced view. Focus a widget and press Shift + Enter, hold Shift while hovering for about 500 ms, or choose Open advanced view from its menu. It is unavailable in edit mode and remains compact-only on widgets that do not opt in.",
} as const;
const standardInteractions = [
@@ -12,19 +12,19 @@ const standardInteractions = [
icon: IconMouse2,
title: "Widget actions",
description:
"Right-click for live query status, refresh, settings, quick options, and actions supported by the widget.",
},
{
icon: IconPlus,
title: "Place it precisely",
description:
"With board changes allowed, right-click or touch and hold empty grid space to add there. Keyboard users can press Shift + F10.",
"Signed-in users can open the widget context menu outside edit mode when Enable right click on widgets is enabled. App tiles are excluded. Settings, quick options, and integration actions appear or enable only when the widget and your board or integration permissions allow them.",
},
{
icon: IconLayoutGrid,
title: "Adaptive detail",
description:
"Resize a widget to tune its density. Compact views keep primary status visible, then reveal details and controls as space or intent allows.",
"Resize a widget to tune its density. Compact layouts hide lower-priority details and controls as space shrinks; compact-only widgets adapt in place instead of opening a separate advanced view.",
},
{
icon: IconRefresh,
title: "Data status",
description:
"Widgets distinguish initial loading, successful empty results, terminal errors, stale cached data, and partial integration failures as applicable. Initial loads use a loading state, empty results use an empty state, and terminal failures can offer retry; retained data can stay visible with a stale warning, while healthy integrations can remain visible when another source fails. The shared menu reports Loading while matching active queries fetch, Idle when none are active, Error when any match is in error, otherwise Success with the latest data age when available. Refresh refetches only active queries owned by that widget and view and is disabled while they fetch.",
},
] as const;
@@ -35,14 +35,12 @@ export const WidgetInteractionGuide = ({ advancedView = true }: { advancedView?:
<details className="group my-6 overflow-hidden rounded-xl border border-solid border-[#e5e7eb] bg-slate-50/70 dark:border-[#333] dark:bg-gray-900/40">
<summary className="flex cursor-pointer list-none items-center justify-between gap-3 px-4 py-3 font-semibold marker:hidden">
<span>Power-user controls</span>
<span className="text-xs font-normal text-[#696969] group-open:hidden dark:text-[#999]">Show shortcuts</span>
<span className="text-xs font-normal text-[#696969] group-open:hidden dark:text-[#999]">Show controls</span>
<span className="hidden text-xs font-normal text-[#696969] group-open:inline dark:text-[#999]">
Hide shortcuts
Hide controls
</span>
</summary>
<div
className={`grid gap-3 border-0 border-t border-solid border-[#e5e7eb] p-4 md:grid-cols-2 ${advancedView ? "xl:grid-cols-4" : "xl:grid-cols-3"} dark:border-[#333]`}
>
<div className="grid gap-3 border-0 border-t border-solid border-[#e5e7eb] p-4 md:grid-cols-2 xl:grid-cols-4 dark:border-[#333]">
{interactions.map(({ icon: Icon, title, description }) => (
<div key={title} className="rounded-lg bg-white p-3 shadow-sm dark:bg-gray-950">
<Icon aria-hidden size={20} stroke={1.5} className="mb-2 stroke-red-500" />
@@ -53,8 +51,13 @@ export const WidgetInteractionGuide = ({ advancedView = true }: { advancedView?:
</div>
{advancedView && (
<p className="m-0 px-4 pb-4 text-xs text-[#696969] dark:text-[#999]">
Advanced view reuses the widget&apos;s live data and cache. The rest of the board stays visible but dimmed. On
touch devices, use the widget menu; compact-only widgets adapt in place instead.
Advanced view reuses existing data where possible; some widgets request additional or denser data. Hold Shift
while hovering a supported widget for a temporary preview; it opens after about 500 ms and closes when Shift
is released, the pointer leaves, or the window loses focus. For a persistent view, focus the widget and press
Shift + Enter or use Open advanced view from its menu. Manual view closes with Escape after nested menus or
popovers handle their own Escape, its close button, or the backdrop; it restores focus, traps focus, and locks
background scrolling. Shift + wheel scrolls a scrollable advanced panel when it can consume the event. On
touch devices, use the widget menu when available; compact-only widgets adapt in place instead.
</p>
)}
</details>