Compare commits
384
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bafe1ce75 | ||
|
|
ca95112466 | ||
|
|
32732680e5 | ||
|
|
4905ea6738 | ||
|
|
ca856f9f54 | ||
|
|
ef2ed6c602 | ||
|
|
9b4970583e | ||
|
|
4641839fa4 | ||
|
|
00e9dbe268 | ||
|
|
34a17932ae | ||
|
|
c224e62f30 | ||
|
|
25c2fbab51 | ||
|
|
5b58be347e | ||
|
|
7fcc36adff | ||
|
|
d11f34a6f2 | ||
|
|
ff8ef7a403 | ||
|
|
76758ff7bc | ||
|
|
45c1507bfa | ||
|
|
1c34d95976 | ||
|
|
eb33ff1372 | ||
|
|
c3a090e880 | ||
|
|
def3a3a6fa | ||
|
|
ad0bb41fe4 | ||
|
|
6e76e8c0dd | ||
|
|
e06d95ee7a | ||
|
|
865e96b5b1 | ||
|
|
66313055df | ||
|
|
2283ec7113 | ||
|
|
e5d6b6be87 | ||
|
|
e3e5730a39 | ||
|
|
d776078ecf | ||
|
|
4ca02a9133 | ||
|
|
f4891447d3 | ||
|
|
2067d851cf | ||
|
|
3aedd312b7 | ||
|
|
e8ca63d9c0 | ||
|
|
d89bf25e21 | ||
|
|
07770e862c | ||
|
|
576cc0b418 | ||
|
|
81d5070a8b | ||
|
|
44df54ecb8 | ||
|
|
07867cc3db | ||
|
|
818e9bb0c5 | ||
|
|
3b73f0bc4c | ||
|
|
c92ca2af5f | ||
|
|
124a5526b4 | ||
|
|
b52b2b5f3c | ||
|
|
1f3e7afd5e | ||
|
|
2c8134ee02 | ||
|
|
0482c040ec | ||
|
|
1088f058e3 | ||
|
|
47861c0194 | ||
|
|
0e7d7e0a2f | ||
|
|
5ca5bd3bda | ||
|
|
bcb732a26c | ||
|
|
8c9e2c8bfa | ||
|
|
14c7e58e17 | ||
|
|
2eea916013 | ||
|
|
5a7607ddf7 | ||
|
|
e466208c09 | ||
|
|
cfce8ada4e | ||
|
|
36661e810f | ||
|
|
308e478733 | ||
|
|
59ce3c7158 | ||
|
|
6ea7a6ff09 | ||
|
|
1611a9033b | ||
|
|
20f4f87361 | ||
|
|
4130b62f24 | ||
|
|
a3b7b0835e | ||
|
|
e225424ab1 | ||
|
|
237b6cb119 | ||
|
|
557b3f8d5f | ||
|
|
8f0efd508e | ||
|
|
876f312a2e | ||
|
|
e662a175c0 | ||
|
|
52c261c034 | ||
|
|
d8108f3375 | ||
|
|
0ae2203aaa | ||
|
|
6b30ba3707 | ||
|
|
2e945805b2 | ||
|
|
780e5928cd | ||
|
|
f61e07ae58 | ||
|
|
7a8b37c906 | ||
|
|
70b0df45b3 | ||
|
|
fa946e1b33 | ||
|
|
f01969b9fc | ||
|
|
11b941285b | ||
|
|
3148ecb823 | ||
|
|
71811dda55 | ||
|
|
e4cb8da5e2 | ||
|
|
aab12bbe64 | ||
|
|
12c436cd5c | ||
|
|
14d426764b | ||
|
|
b3b5a58116 | ||
|
|
5f7429063b | ||
|
|
2ffb05e746 | ||
|
|
921aac0560 | ||
|
|
24bd63d216 | ||
|
|
8b87967a12 | ||
|
|
d44db87d43 | ||
|
|
73b7df44d7 | ||
|
|
c6cf75e561 | ||
|
|
3c3232e85a | ||
|
|
c1aaa1a955 | ||
|
|
5e6ebef961 | ||
|
|
c3436d43e6 | ||
|
|
d825cb7051 | ||
|
|
de682abadc | ||
|
|
6afa4d2f4a | ||
|
|
95473d1b7f | ||
|
|
fd58f9a358 | ||
|
|
a89877b461 | ||
|
|
b362ed1e68 | ||
|
|
83355510c0 | ||
|
|
6cf34af845 | ||
|
|
0f2f9e6e59 | ||
|
|
2f5f89426a | ||
|
|
427db07a95 | ||
|
|
fc07e911d1 | ||
|
|
94ec244fad | ||
|
|
4184721b9a | ||
|
|
8dc5a0540f | ||
|
|
71d1cef139 | ||
|
|
a97605e0f7 | ||
|
|
0876992a9a | ||
|
|
5a65ff47bf | ||
|
|
4060f9299f | ||
|
|
ca8163c061 | ||
|
|
cb1a73a6ac | ||
|
|
7899d8677c | ||
|
|
16e3f54512 | ||
|
|
39a14a18f9 | ||
|
|
ed705c9e34 | ||
|
|
b0ec746081 | ||
|
|
00fa1192d7 | ||
|
|
c7b0c1bcf0 | ||
|
|
9da3ff3e9d | ||
|
|
93113234a7 | ||
|
|
9ef4bf803d | ||
|
|
4920063318 | ||
|
|
68d236fd63 | ||
|
|
ebce24ba67 | ||
|
|
039bfb9b93 | ||
|
|
faf01e82c8 | ||
|
|
54d7821e24 | ||
|
|
f29e604bc1 | ||
|
|
50deb638fb | ||
|
|
b1f25f779d | ||
|
|
df874dd2db | ||
|
|
26ede97407 | ||
|
|
b5fbf0085f | ||
|
|
a65dc2841f | ||
|
|
09a85e69c1 | ||
|
|
e53f63b373 | ||
|
|
ea1e07c74f | ||
|
|
4635e3ce4e | ||
|
|
345d9e6fa4 | ||
|
|
f86120016c | ||
|
|
68b70ac01a | ||
|
|
c65e6cdbd4 | ||
|
|
073d9ce127 | ||
|
|
6da471d24a | ||
|
|
c157001455 | ||
|
|
00e4cc8100 | ||
|
|
d3205d886a | ||
|
|
97d26bde5e | ||
|
|
12f950ce4a | ||
|
|
381a257dbc | ||
|
|
16c86079e6 | ||
|
|
fd69cd28f0 | ||
|
|
3b6de1f91f | ||
|
|
f0747fd28b | ||
|
|
5c96c52e64 | ||
|
|
bc2aff17f3 | ||
|
|
90e30ce577 | ||
|
|
485a54d6d8 | ||
|
|
45954837ed | ||
|
|
aef0a6d340 | ||
|
|
e74d5e2aae | ||
|
|
7453e6c559 | ||
|
|
b3ec1381e9 | ||
|
|
30c00ea30d | ||
|
|
4df62221c7 | ||
|
|
dec04da806 | ||
|
|
866b3ab78b | ||
|
|
92c6e4668d | ||
|
|
df70d06606 | ||
|
|
4d0c2f8eec | ||
|
|
9887c018d2 | ||
|
|
e93f14acdf | ||
|
|
85db534fa7 | ||
|
|
8ac35083e6 | ||
|
|
f481007b9a | ||
|
|
f88fbb5a19 | ||
|
|
d4cecde0f0 | ||
|
|
0356eb427c | ||
|
|
b3e6d35b79 | ||
|
|
be9c69f325 | ||
|
|
e93293f8a9 | ||
|
|
967ed8dd73 | ||
|
|
5454bcc8ac | ||
|
|
ec54ddcfa6 | ||
|
|
25a4aca8c0 | ||
|
|
d1f76b24c0 | ||
|
|
6a2e7d0610 | ||
|
|
438f0d80eb | ||
|
|
673b1a74d6 | ||
|
|
84db2f9d49 | ||
|
|
2e3dca3e87 | ||
|
|
7d5ec15017 | ||
|
|
f6baaa0c3b | ||
|
|
9d3c0acb95 | ||
|
|
3079eb1767 | ||
|
|
5059a16181 | ||
|
|
444ae82352 | ||
|
|
65a7e84bea | ||
|
|
55226842f9 | ||
|
|
e84b05e91c | ||
|
|
d6359d595c | ||
|
|
f876b2fd06 | ||
|
|
c256bd644c | ||
|
|
c9fe69c5ec | ||
|
|
64a57bac82 | ||
|
|
39e408c53f | ||
|
|
f404f77002 | ||
|
|
21a38b3004 | ||
|
|
3b76381262 | ||
|
|
bb97ce5a01 | ||
|
|
2e323677f4 | ||
|
|
f2dd7c10a2 | ||
|
|
ccabc2dab6 | ||
|
|
5633bc4775 | ||
|
|
d20bb18b08 | ||
|
|
747ccdfa9e | ||
|
|
bbcfc505bc | ||
|
|
2b1053e243 | ||
|
|
154753ddda | ||
|
|
74599b0b65 | ||
|
|
7bfd10210e | ||
|
|
19befee8a5 | ||
|
|
77cbd9f723 | ||
|
|
1fa345edd1 | ||
|
|
6331c88911 | ||
|
|
4c69c4ec45 | ||
|
|
ff31208c37 | ||
|
|
177ea49469 | ||
|
|
f03bf6a917 | ||
|
|
80d1416ad6 | ||
|
|
d2a935b6c1 | ||
|
|
758c300d9e | ||
|
|
53185701ce | ||
|
|
21cc0b19b3 | ||
|
|
a882a227f1 | ||
|
|
e535ff393e | ||
|
|
349dfa6d41 | ||
|
|
6c8dbc182f | ||
|
|
d880c4a281 | ||
|
|
d7083fc14c | ||
|
|
c7431ef1f5 | ||
|
|
419c8434f9 | ||
|
|
49ef7748ad | ||
|
|
a9f1293077 | ||
|
|
2344896ba9 | ||
|
|
0aa3b98675 | ||
|
|
f003d9ddc9 | ||
|
|
56346b2aa8 | ||
|
|
e321ee27a2 | ||
|
|
afd73565c9 | ||
|
|
36d7ef0562 | ||
|
|
99a871344c | ||
|
|
010ba45468 | ||
|
|
777bbd89a1 | ||
|
|
6f2cd7fc2b | ||
|
|
1a5e083fd0 | ||
|
|
9607bf65cd | ||
|
|
6caa1c8224 | ||
|
|
556179deb5 | ||
|
|
ab012b4a32 | ||
|
|
d635e4d80f | ||
|
|
6afd21fe98 | ||
|
|
cf7ec0a31f | ||
|
|
5be2226c39 | ||
|
|
082f2271c5 | ||
|
|
5a03640a1b | ||
|
|
80132ae71f | ||
|
|
9b8b6c7cfe | ||
|
|
282997e016 | ||
|
|
c973f54c8e | ||
|
|
8dd984fcb3 | ||
|
|
2d17e9f58a | ||
|
|
3fe3d1714a | ||
|
|
add61ce9a3 | ||
|
|
1332f5626b | ||
|
|
203711e570 | ||
|
|
2cb69c4ea3 | ||
|
|
7368262149 | ||
|
|
b9858b5ba4 | ||
|
|
b84502ef9b | ||
|
|
b39bff43a9 | ||
|
|
d5dbf6fc17 | ||
|
|
c6c96c4870 | ||
|
|
f438ea8aa9 | ||
|
|
279aeb3269 | ||
|
|
f5ac662aeb | ||
|
|
0b1c2f856f | ||
|
|
dd11fd8381 | ||
|
|
07e6598f75 | ||
|
|
b7eb099977 | ||
|
|
5ab3ee7d02 | ||
|
|
59106053f2 | ||
|
|
078c41b6ec | ||
|
|
1f5311e93b | ||
|
|
5bde4d3cc5 | ||
|
|
94074c368d | ||
|
|
bf91f92d56 | ||
|
|
b7693c4238 | ||
|
|
fb18ac8fc5 | ||
|
|
336ba76fef | ||
|
|
0110a2ef63 | ||
|
|
5dc4dbeaef | ||
|
|
f3f2778f1f | ||
|
|
c08b8979ee | ||
|
|
9a7a828618 | ||
|
|
352a8ed0d0 | ||
|
|
6d3f7e3e36 | ||
|
|
7833cca86b | ||
|
|
6edf78acd7 | ||
|
|
7d43f6e5d9 | ||
|
|
1723a9e744 | ||
|
|
2ba31fd37b | ||
|
|
327e5c15ae | ||
|
|
3efe64a062 | ||
|
|
40b726b8bc | ||
|
|
d2d0d16e4d | ||
|
|
63ad879022 | ||
|
|
2ae0c2ab43 | ||
|
|
2410ba5f5c | ||
|
|
8f0039245d | ||
|
|
0871e4cf0d | ||
|
|
db9f42520a | ||
|
|
3ebcedec26 | ||
|
|
c41a3d427e | ||
|
|
71c39aa18e | ||
|
|
000c51a131 | ||
|
|
2c44c7f2b8 | ||
|
|
c2cf25b4fb | ||
|
|
03ebb8c2db | ||
|
|
f0b153aa37 | ||
|
|
965d130ce7 | ||
|
|
a87a16edb1 | ||
|
|
1493252e28 | ||
|
|
46ff20b0a0 | ||
|
|
1bc84dd90e | ||
|
|
14cbc03621 | ||
|
|
9ec35b0cda | ||
|
|
2d6dd2f0d0 | ||
|
|
572bc71396 | ||
|
|
d4b1739c46 | ||
|
|
76495948e2 | ||
|
|
a95052c787 | ||
|
|
400127b45c | ||
|
|
f77c47b94b | ||
|
|
825636c4f6 | ||
|
|
3aa14e22ed | ||
|
|
acdfcbb18e | ||
|
|
3a1245cbc5 | ||
|
|
f0963405ca | ||
|
|
d078d761d9 | ||
|
|
84fcf02729 | ||
|
|
c308ae5c6e | ||
|
|
93192cfedc | ||
|
|
0f9ae13560 | ||
|
|
f4a6f37415 | ||
|
|
95bfaf5127 | ||
|
|
83e3b22f9a | ||
|
|
cd71404288 | ||
|
|
7a089ab177 | ||
|
|
2513e8454f | ||
|
|
527e911eef | ||
|
|
ff2cdecc86 | ||
|
|
742bde1d27 | ||
|
|
64febbce12 | ||
|
|
98bb1aecf8 | ||
|
|
06c489bd48 |
No files matched your search
@@ -7,11 +7,15 @@ description: Author safe Homarr Custom JSX v2 widgets from API documentation.
|
||||
|
||||
Author one widget at a time. When connected to Homarr, retrieve `homarr://custom-widgets/schema` and only the component resources needed for the design. The live resources are authoritative for the installed release.
|
||||
|
||||
Keep documentation retrieval targeted: fetch at most eight named component documents before the first validation, or at most four after loading a complete example. Fetch another component only when a validation issue or a concrete missing interaction requires it; never load the catalog component-by-component.
|
||||
|
||||
1. Read the API documentation.
|
||||
2. Create one credential-free widget with keyed `sources`, keyed `requests`, optional keyed `options`, and safe JSX `template`.
|
||||
3. Validate, preview, test queries and simulated actions, visually inspect, and iterate.
|
||||
4. Configure deployment-specific source URLs and request credentials through Homarr; never repeat plaintext.
|
||||
5. Save only after narrow/wide, light/dark, loading, empty, error, and success states work.
|
||||
5. Save only after narrow/wide, light/dark, loading, empty, error, and success states work. When connected through MCP, persist the exact final tested preview with `customWidget_createFromPreview`; do not resend a large template through `customWidget_create` unless no preview session is available.
|
||||
|
||||
Treat a supplied sample or successful preview response as the binding contract. Render every core requested field, guard optional arrays and nested values before indexing, and do not silently drop returned items. Show concise freshness context when the API supplies a timestamp. Give recoverable load errors and empty states a clear refresh or retry path.
|
||||
|
||||
Use `{option:name}` or `$option` for saved options. Use `{param:name}` or `$param` for values supplied by `SubFetch`, `ActionButton`, or `ToggleSwitch`. Load queries cannot use invocation parameters. Templates read `data`, `status`, `options`, and temporary `inputs`.
|
||||
|
||||
@@ -19,6 +23,10 @@ Use `{option:name}` or `$option` for saved options. Use `{param:name}` or `$para
|
||||
|
||||
Use standard Mantine compound names and deliberate visual hierarchy, spacing, restrained semantic color, and responsive layouts. Avoid excessive nested cards.
|
||||
|
||||
Aim for distinctive polish without decorative gradients or fragile custom styling: use one semantic accent, clear surface contrast, purposeful typography, and compact rows that remain scannable in narrow tiles. Give standalone icons an accessible label or pair them with visible text.
|
||||
|
||||
Make initial states actionable with an example, useful hint, or clear next step. Use wrapping groups for variable-length labels and values on narrow tiles. Do not use an unlabeled decorative icon as an empty state.
|
||||
|
||||
Do not use imports, hooks, refs, raw HTML, raw event callbacks, browser requests, arbitrary functions, eval, bigint, npm packages, authored statement blocks, IIFEs, or recursion. Do not pretend MCP tools exist in an offline chat session.
|
||||
|
||||
To install an existing widget, use `customWidget_workshopSearch`, inspect it with `customWidget_workshopGet`, then use `customWidget_workshopInstall`. Configure each self-hosted source afterward with `customWidget_sourceConfigure`, or call `customWidget_configurationRequestUser` so the user can enter the server URL and credentials directly in Homarr.
|
||||
|
||||
@@ -66,8 +66,6 @@ homarr/
|
||||
│ ├── analytics/ # Server-side analytics (Umami)
|
||||
│ ├── server-settings/ # Server setting keys/types
|
||||
│ ├── settings/ # User-facing settings UI context
|
||||
│ ├── old-import/ # Legacy Homarr import
|
||||
│ ├── old-schema/ # Legacy Homarr zod schemas
|
||||
│ └── cli/ # Node CLI for ops (brocli)
|
||||
├── tooling/
|
||||
│ ├── typescript/ # Base tsconfig
|
||||
|
||||
@@ -263,15 +263,6 @@ Even hidden options need translation keys or the translation test fails:
|
||||
"widget.mywidget.option.columnWidths.label": "Column widths"
|
||||
```
|
||||
|
||||
### Old-import mappings
|
||||
|
||||
Add to `packages/old-import/src/widgets/options.ts`:
|
||||
|
||||
```typescript
|
||||
columnOrder: () => undefined,
|
||||
columnWidths: () => undefined,
|
||||
```
|
||||
|
||||
### Dynamic storeKey
|
||||
|
||||
Use a key that includes `itemId` and sorted column names. This prevents cross-widget localStorage bleeding:
|
||||
@@ -610,13 +601,12 @@ if (isEditMode) rowContextMenuHandler = undefined;
|
||||
4. Add CSS file for transparent backgrounds (if widget context)
|
||||
5. Add responsive column visibility (if widget context)
|
||||
6. Add hidden options + hydration logic (if column persistence needed)
|
||||
7. Add old-import mappings for new options
|
||||
8. Add translation keys (including hidden option labels)
|
||||
9. Add context menu if row actions exist
|
||||
10. Add row expansion if detail view exists
|
||||
11. Add error handling on all mutations
|
||||
12. Add accessibility attributes
|
||||
13. Test at multiple widget widths
|
||||
14. Update docs in `apps/docs/docs/widgets/` if applicable
|
||||
15. Run `pnpm turbo typecheck` — catches old-import mismatches
|
||||
16. Run translation spec — catches missing keys
|
||||
7. Add translation keys (including hidden option labels)
|
||||
8. Add context menu if row actions exist
|
||||
9. Add row expansion if detail view exists
|
||||
10. Add error handling on all mutations
|
||||
11. Add accessibility attributes
|
||||
12. Test at multiple widget widths
|
||||
13. Update docs in `apps/docs/docs/widgets/` if applicable
|
||||
14. Run `pnpm turbo typecheck`
|
||||
15. Run translation spec — catches missing keys
|
||||
@@ -95,6 +95,12 @@ jobs:
|
||||
- name: Validate checked-in OpenAPI schema
|
||||
run: pnpm check:openapi
|
||||
|
||||
- name: Validate feature contracts
|
||||
run: pnpm check:feature-contracts
|
||||
|
||||
- name: Test feature platform
|
||||
run: pnpm test:feature-platform
|
||||
|
||||
- name: Validate Custom Widget bundle budget
|
||||
run: |
|
||||
pnpm --filter nextjs build
|
||||
|
||||
@@ -84,3 +84,11 @@ apps/docs/.docusaurus
|
||||
apps/docs/build
|
||||
.pnpm-store
|
||||
benchmark-results
|
||||
|
||||
# local performance investigation (never ship benchmark data or restored backups)
|
||||
.widget-performance/
|
||||
.bench/
|
||||
homarr-backup*.zip
|
||||
*.sqlite-wal
|
||||
*.sqlite-shm
|
||||
/scripts/benchmarks/
|
||||
@@ -40,8 +40,6 @@ homarr/
|
||||
│ ├── analytics/ # Server-side analytics (Umami)
|
||||
│ ├── server-settings/ # Server setting keys/types
|
||||
│ ├── settings/ # User-facing settings UI context
|
||||
│ ├── old-import/ # Legacy Homarr import
|
||||
│ ├── old-schema/ # Legacy Homarr zod schemas
|
||||
│ └── cli/ # Node CLI for ops (brocli)
|
||||
├── tooling/
|
||||
│ ├── typescript/ # Base tsconfig
|
||||
|
||||
@@ -206,6 +206,14 @@ packages/integrations/src/unraid @manuel-rw
|
||||
apps/docs/docs/integrations/uptime-kuma @ajnart
|
||||
packages/integrations/src/uptime-kuma @ajnart
|
||||
|
||||
# What's Up Docker (WUD) - introduced in https://github.com/homarr-labs/homarr/pull/6523 by @tiimonn
|
||||
apps/docs/docs/integrations/whats-up-docker @tiimonn
|
||||
apps/docs/docs/widgets/whats-up-docker @tiimonn
|
||||
packages/api/src/router/widgets/wud.ts @tiimonn
|
||||
packages/integrations/src/wud @tiimonn
|
||||
packages/request-handler/src/wud.ts @tiimonn
|
||||
packages/widgets/src/wud @tiimonn
|
||||
|
||||
# Traefik - by @Nerdy-Technician
|
||||
apps/docs/docs/integrations/traefik @Nerdy-Technician
|
||||
apps/docs/docs/widgets/traefik @Nerdy-Technician
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ For a single consolidated file with all content, use:
|
||||
- [How to integrate custom input with use-form hook?](https://mantine.dev/llms/q-custom-input-use-form.md): Learn how to add use-form support for custom inputs
|
||||
- [How to prevent Modal from closing?](https://mantine.dev/llms/q-how-to-prevent-modal-from-closing.md): Learn how to prevent Modal from closing when the user clicks outside of it or presses the Escape key
|
||||
- [How to scroll to the top of the form if the form is submitted with errors?](https://mantine.dev/llms/q-scroll-to-the-top-of-the-form.md): Learn how to call a function when the form is submitted with errors
|
||||
- [How to update Mantine dependencies?](https://mantine.dev/llms/q-how-to-update-dependencies.md): Learn how to update @mantine/_, @mantinex/_ and postcss-preset-mantine dependencies manually or with ncu script
|
||||
- [How to update Mantine dependencies?](https://mantine.dev/llms/q-how-to-update-dependencies.md): Learn how to update `@mantine/*`, `@mantinex/*` and `postcss-preset-mantine` dependencies manually or with ncu script
|
||||
- [How to use Dropzone with @mantine/form?](https://mantine.dev/llms/q-how-to-use-dropzone-with-form.md): Learn how to use Dropzone with @mantine/form to handle file selection state
|
||||
- [How to use Mantine template on GitHub?](https://mantine.dev/llms/q-templates-usage.md): Learn how to create a new repository based on Mantine template
|
||||
- [I get hydration warning about data-mantine-color-scheme attribute, what does it mean?](https://mantine.dev/llms/q-color-scheme-hydration-warning.md): Learn how hydration works and how to fix warnings
|
||||
|
||||
+2
-2
@@ -61,8 +61,8 @@ pnpm typecheck
|
||||
docs/
|
||||
├── getting-started/ # Installation guides, glossary, prerequisites
|
||||
├── management/ # Boards, apps, users, integrations, settings, tasks, API
|
||||
├── integrations/ # 55 integration guides (one folder each)
|
||||
├── widgets/ # 42 widget guides (one folder each)
|
||||
├── integrations/ # 56 integration guides (one folder each)
|
||||
├── widgets/ # 43 widget guides (one folder each)
|
||||
├── advanced/ # SSO, env vars, proxy, styling, CLI, development
|
||||
└── community/ # FAQ, donate, license, translations, get-in-touch
|
||||
```
|
||||
|
||||
@@ -13,8 +13,6 @@ This will be a one time occurrence and version > ``1.0`` will have less breaking
|
||||
This time more important than every; **read the breaking changes** list and **create a backup** before updating.
|
||||
Some third party installation methods may take multiple days or weeks to update to the new version.
|
||||
|
||||
See [this guide on how to import your previous Homarr dashboard](/docs/getting-started/after-the-installation#importing-a-zip-from-version-before-100)
|
||||
|
||||
## Changes at a glance
|
||||
- **Asynchronous integrations system**: In versions prior to ``1.0``, we fetched data upon each request synchronously.
|
||||
This means that the user had to wait for the requests to finish so any data would appear.
|
||||
|
||||
@@ -10,12 +10,11 @@ If you are still on a version before 0.15.10, please update to 0.15.10 where we
|
||||
|
||||
<!-- truncate -->
|
||||
|
||||
## Export your data
|
||||
## Preserve your old installation
|
||||
|
||||
As 1.0 is a complete rewrite of Homarr and therefor not compatible with the old docker mounts, we need to export your data first.
|
||||
For this you can navigate to the `Migrate to 1.0` page which you can find under `Management` > `Tools` > `Migrate to 1.0`.
|
||||
Here you can select which things you want to export and download the zip file.
|
||||
It will also show you a secret key which you will need to import the secret data like user password hashes, salts and integration secrets.
|
||||
Homarr 1.0 is a complete rewrite and cannot reuse the old Docker mounts. Keep the 0.15 installation and its data intact while you build the replacement. The historical `Management` > `Tools` > `Migrate to 1.0` export can still be kept as a record, but current Homarr releases cannot import that ZIP or its secret key.
|
||||
|
||||
Plan to recreate your apps, integrations, and boards manually in the new installation. A SQLite database backup can be restored from the onboarding welcome screen only when it was created by a compatible current Homarr release; it is not a conversion path for a 0.15 database.
|
||||
|
||||

|
||||
|
||||
@@ -28,28 +27,15 @@ Please note that if you are using docker you need to specify a diffrent external
|
||||
So in your compose file the ports would be `7576:7575` instead of `7575:7575`.
|
||||
|
||||
## Import your data
|
||||
Once the installation is completed you can access the new installation. During the onboarding you'll need to select `Import from Homarr before 1.0`.
|
||||
|
||||

|
||||
:::warning Legacy guide
|
||||
|
||||
After that you can upload the zip file you downloaded in the first step.
|
||||
The pre-1.0 ZIP importer is no longer included in current Homarr releases. Keep the old installation available until you have manually verified the replacement. Compatible SQLite backups from current releases can still be restored from the onboarding welcome screen.
|
||||
|
||||

|
||||
|
||||
After that you'll get a list with all your boards that can be imported. You can configure how it should be imported and can select which screensizes it should import.
|
||||
|
||||

|
||||
|
||||
Once you have selected everything you can click on `Confirm import and continue` which will open the encryption key input dialog where you can input the secret key you got during the export.
|
||||
|
||||

|
||||
|
||||
After that you can continue the onboarding and you should see all your boards and data in the new installation.
|
||||
|
||||
See [After the installation](https://homarr.dev/docs/getting-started/after-the-installation) for more information.
|
||||
:::
|
||||
|
||||
## Remove the old installation
|
||||
|
||||
After you are happy with the new installation and everything is working as expected you can remove the old installation.
|
||||
Please make sure to backup the data before you remove the old installation.
|
||||
Also once the port 7575 is free again you can change the port mapping of the new installation to `7575:7575` if you want to.
|
||||
Also once the port 7575 is free again you can change the port mapping of the new installation to `7575:7575` if you want to.
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"label": "Advanced",
|
||||
"collapsed": true,
|
||||
"position": 4,
|
||||
"position": 5,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"title": "Advanced Concepts or features"
|
||||
|
||||
@@ -57,51 +57,73 @@ Homarr becomes the instance administrator.
|
||||
Use `pnpm docker:dev:up` instead when you only need Redis in the background. Running the migration also seeds the
|
||||
default data; use `pnpm db:seed` to seed it explicitly later.
|
||||
|
||||
### Run PocketBase and documentation
|
||||
Workshop contributors can set up PocketBase and Docusaurus with the dedicated
|
||||
[Workshop development guide](../../workshop/development).
|
||||
|
||||
Start PocketBase in another terminal:
|
||||
### Scaffold a native feature
|
||||
|
||||
Run `pnpm generate:feature` and choose `native-integration`, `widget`, or `integration-widget`. The generator creates
|
||||
implementation, focused test, documentation, and English translation skeletons and updates the current explicit
|
||||
registries. It stops before writing if a target exists, a registry anchor is ambiguous, or a supported integration is
|
||||
unknown.
|
||||
|
||||
The generated code is a starting contract, not a finished feature. Replace the placeholder request, UI, tests, secrets,
|
||||
categories, configuration, error states, and documentation with behavior specific to the service. Then run:
|
||||
|
||||
```sh
|
||||
cp apps/workshop/.env.example apps/workshop/.env
|
||||
docker compose --env-file apps/workshop/.env -f apps/workshop/docker-compose.yml up --build workshop
|
||||
pnpm check:feature-contracts
|
||||
pnpm test:feature-platform
|
||||
```
|
||||
|
||||
Then start Docusaurus in a third terminal:
|
||||
The contract check reports the exact registry, docs path, translation, support mapping, or package export that needs
|
||||
repair. It intentionally follows today's explicit registries; a future canonical feature descriptor can replace those
|
||||
patches without changing the contributor-facing generator names.
|
||||
|
||||
Integration-capable widgets declare their server-safe relationship in `nativeFeatureCapabilities`. Homarr derives the
|
||||
widget-to-integration map, reverse integration-to-widget map, optional-connection set, and server selection limits from
|
||||
that descriptor. Widget definitions still mirror the supported list for client-side type inference; the focused widget
|
||||
manifest test and `check:feature-contracts` prevent drift until those definition types can consume the descriptor
|
||||
directly.
|
||||
|
||||
New integration skeletons also include response fixtures using `simulateResponseContractAsync`. Replace the generated
|
||||
minimal schema and fixtures with representative upstream payloads: at least one successful response, important optional
|
||||
or version-specific shapes, and a malformed response that must be rejected. The simulator uses the real parser without
|
||||
network access and reports every fixture mismatch, so parser contracts can be reviewed before credentials are available.
|
||||
|
||||
### Review native feature states
|
||||
|
||||
In a development server, administrators can open `/manage/tools/feature-workbench` to inspect the shared `CatalogItem`
|
||||
and integration response-contract fixtures. The workbench uses the application Mantine theme and the same segmented
|
||||
fixture controls as the custom-widget preview. It covers ready, setup-required, standalone, connection-error, selected,
|
||||
loading, disabled, parser-success, and parser-failure states without requiring a live service.
|
||||
|
||||
Use the workbench after changing shared catalog presentation or generated parser contracts, then run the focused test:
|
||||
|
||||
```sh
|
||||
WORKSHOP_API_URL=http://127.0.0.1:8090 pnpm dev:docs
|
||||
pnpm exec vitest run "apps/nextjs/src/app/[locale]/manage/tools/feature-workbench/_feature-state-workbench.spec.tsx"
|
||||
```
|
||||
|
||||
PocketBase runs on port 8090 and Docusaurus on port 3003. Stopping the services preserves the PocketBase volume.
|
||||
The workbench is deliberately development-only and is not linked from management navigation. It does not yet exercise
|
||||
real integration branding, long translated labels, right-to-left text, high-contrast screenshots, touch interaction,
|
||||
small viewport clipping, live network latency, retry transitions, or service-specific response schemas. Those remain
|
||||
manual review items until screenshot and browser-automation coverage is added.
|
||||
|
||||
Open `http://127.0.0.1:8090/_/` and create the first PocketBase superuser. The Workshop UI is available at
|
||||
`http://127.0.0.1:3003/workshop`.
|
||||
### Check one native feature
|
||||
|
||||
Point Homarr at the local services with:
|
||||
|
||||
```dotenv
|
||||
HOMARR_WEBSITE_URL=http://127.0.0.1:3003
|
||||
WORKSHOP_API_URL=http://127.0.0.1:8090
|
||||
WORKSHOP_WEB_URL=http://127.0.0.1:3003/workshop
|
||||
```
|
||||
|
||||
Restart `pnpm dev` after changing `.env`. GitHub OAuth is optional locally. See
|
||||
[`apps/workshop/README.md`](https://github.com/homarr-labs/homarr/blob/HEAD/apps/workshop/README.md) for OAuth,
|
||||
Workshop moderator promotion, logs, tests, backup, restore, stopping, and intentional volume reset.
|
||||
|
||||
### URL testing
|
||||
|
||||
Docusaurus reads `HOMARR_WEBSITE_URL` and `WORKSHOP_API_URL` when it starts or builds. Homarr also reads
|
||||
`WORKSHOP_WEB_URL` when its server starts, so these values do not rely on `NEXT_PUBLIC_*` build variables. Test a
|
||||
separate API origin before release:
|
||||
Run `pnpm check:feature <kind>` before submitting an integration or widget change. The command resolves the kind through
|
||||
`integrationDefs`, `widgetKinds`, the widget manifest, creator registry, documentation slug maps, and capability
|
||||
descriptor. When an integration and widget share a kind, such as `wud`, both sides are checked together.
|
||||
|
||||
```sh
|
||||
HOMARR_WEBSITE_URL=http://127.0.0.1:3003 \
|
||||
WORKSHOP_API_URL=http://127.0.0.1:8090 \
|
||||
WORKSHOP_WEB_URL=http://127.0.0.1:3003/workshop \
|
||||
pnpm dev:docs
|
||||
pnpm check:feature wud --plan
|
||||
pnpm check:feature wud
|
||||
```
|
||||
|
||||
`--plan` (or `--dry-run`) prints the deterministic affected files and commands without executing them. The normal mode
|
||||
runs feature contracts, nearby fixture and unit tests, affected package typechecks, and lint and formatting for only the
|
||||
resolved files. Unknown kinds fail before any command runs. Live-service, container, and visual checks are printed as
|
||||
optional and are never executed implicitly because they may require credentials, images, or a running development app.
|
||||
|
||||
### Useful commands
|
||||
|
||||
- `pnpm dev` — Next.js on port 3000.
|
||||
@@ -112,17 +134,17 @@ pnpm dev:docs
|
||||
- `pnpm dev:cli -- rebuild <name>` — rebuild an image from its recorded checkout or pull request.
|
||||
- `pnpm dev:cli:install` — optionally install the developer CLI as a `homarr` binary.
|
||||
- `pnpm dev:docs` — Docusaurus only on port 3003.
|
||||
- `pnpm docker:workshop` — local PocketBase through Docker Compose.
|
||||
- `pnpm docker:dev:up` — Redis development service in the background.
|
||||
- `pnpm cli` — Homarr operations CLI.
|
||||
- `pnpm db:migration:sqlite:run` — create or update a SQLite database.
|
||||
- `pnpm db:seed` — seed the default database data explicitly.
|
||||
- `pnpm db:studio` — inspect the database.
|
||||
- `pnpm format` / `pnpm format:fix` — check or apply oxfmt.
|
||||
- `pnpm generate:feature` — scaffold a native integration, widget, or paired feature.
|
||||
- `pnpm check:feature-contracts` — validate native feature registries, docs, translations, support maps, and exports.
|
||||
- `pnpm lint` / `pnpm lint:fix` — check or apply oxlint.
|
||||
- `pnpm typecheck` — typecheck all workspaces.
|
||||
- `pnpm test` — unit and contract suites.
|
||||
- `pnpm test:workshop` — disposable Workshop integration suite.
|
||||
- `pnpm build` — production monorepo build.
|
||||
- `docker build -t homarr .` — build a production Docker image.
|
||||
- `docker run -p 7575:7575 -e SECRET_ENCRYPTION_KEY='your_64_character_hex_string' homarr:<name>` — run it.
|
||||
@@ -37,6 +37,9 @@ Using the `PUID` and `PGID` will require you to set the correct permissions on t
|
||||
| `WORKSHOP_API_URL` | PocketBase API used by Homarr and standalone documentation. | HTTP(S) URL | `HOMARR_WEBSITE_URL` |
|
||||
| `WORKSHOP_WEB_URL` | Public Workshop page opened from Homarr. | HTTP(S) URL | `HOMARR_WEBSITE_URL/workshop` |
|
||||
|
||||
Workshop service operators can find its server-only configuration in the
|
||||
[Workshop operator guide](../../workshop/operator#environment-variables).
|
||||
|
||||
## Authentication
|
||||
|
||||
See [Single Sign-On](/docs/advanced/single-sign-on) for more informations.
|
||||
@@ -55,11 +58,12 @@ A random secret can be generated by using the following command: `openssl rand -
|
||||
|
||||
## Docker
|
||||
|
||||
| Environment Variable | Description | Possible values | Default |
|
||||
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ------- |
|
||||
| `DOCKER_HOSTNAMES` | Comma-separated list of hostnames to connect to | For example `localhost,docker.example.com` | - |
|
||||
| `DOCKER_PORTS` | Comma-separated list of ports to connect to | For example `2375,2376` | - |
|
||||
| `DOCKER_SOCKET_PATHS` | Comma-separated list of mounted Docker-compatible socket paths. See the [Docker and Podman integration guide](/docs/integrations/docker) | For example `/var/run/docker.sock,/var/run/podman.sock` | - |
|
||||
| Environment Variable | Description | Possible values | Default |
|
||||
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ------- |
|
||||
| `DOCKER_ENDPOINTS` | JSON array of named, capability-scoped Docker or Podman endpoints. Supports socket, explicitly insecure TCP, and TLS transports. See the [Docker and Podman integration guide](/docs/integrations/docker) | A JSON array of endpoint descriptors | - |
|
||||
| `DOCKER_HOSTNAMES` | Legacy comma-separated list of plaintext TCP hostnames. Ignored when `DOCKER_ENDPOINTS` is set | For example `localhost,docker.example.com` | - |
|
||||
| `DOCKER_PORTS` | Legacy comma-separated list of plaintext TCP ports. Ignored when `DOCKER_ENDPOINTS` is set | For example `2375,2376` | - |
|
||||
| `DOCKER_SOCKET_PATHS` | Legacy comma-separated list of mounted Docker-compatible socket paths. Ignored when `DOCKER_ENDPOINTS` is set | For example `/var/run/docker.sock,/var/run/podman.sock` | - |
|
||||
|
||||
## Database
|
||||
|
||||
@@ -109,12 +113,9 @@ External redis is currently not supported with Proxmox Community Scripts
|
||||
The advanced deployments environment variables should only be used if you know what you are doing.
|
||||
:::
|
||||
|
||||
| Environment Variable | Description |
|
||||
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `DB_MIGRATIONS_DISABLED` | Disable db migrations. For example for helm charts. |
|
||||
| `WORKSHOP_PUBLIC_ORIGIN` | Origin used in PocketBase-generated links and email. It must be set explicitly in production and must match the externally reachable Workshop origin. |
|
||||
| `PB_ALLOWED_ORIGINS` | Browser origins accepted by PocketBase. Use `*` for the central public community so arbitrary self-hosted Homarr instances can use the API and OAuth; private staging may use a comma-separated origin allowlist. |
|
||||
| Environment Variable | Description |
|
||||
| ------------------------ | --------------------------------------------------- |
|
||||
| `DB_MIGRATIONS_DISABLED` | Disable db migrations. For example for helm charts. |
|
||||
|
||||
All public URL variables reject non-HTTP protocols, embedded credentials, query strings, and fragments, and normalize
|
||||
trailing slashes when Homarr or Docusaurus starts. The Docusaurus-only `WORKSHOP_URL` name is accepted as a deprecated alias for
|
||||
`WORKSHOP_API_URL` for one release. `WORKSHOP_API_URL` takes precedence.
|
||||
trailing slashes when Homarr or Docusaurus starts.
|
||||
@@ -12,7 +12,7 @@ tags:
|
||||
|
||||
Icons in Homarr are automatically requested from multiple sources:
|
||||
|
||||
- [Dashboard Icons](https://dashboardicons.com/) - Our recommended source with over 1888+ curated, high-quality icons
|
||||
- [Dashboard Icons](https://dashboardicons.com/) - Our recommended source with more than 1,888 curated, high-quality icons
|
||||
- [selfh.st icons](https://github.com/selfhst/icons)
|
||||
- [Simple icons](https://github.com/simple-icons/simple-icons)
|
||||
- [Tabler icons](https://tabler-icons.io/)
|
||||
@@ -21,7 +21,7 @@ Icons in Homarr are automatically requested from multiple sources:
|
||||
- **Local icons**: automatically fetched from the [medias feature](/docs/management/media/)
|
||||
|
||||
Using these icon sources, Homarr provides a total collection of over 11,000 icons at your fingertips.
|
||||
The icon picker is a fast searcher that helps you to find your desired icon simply by entering a search term.
|
||||
The image picker searches this collection after a short typing delay. When a name is available, such as an app or search engine name, the picker uses it as the initial search. Results show your locally uploaded images first, followed by SVG icons and then other image formats.
|
||||
|
||||
### Finding Missing Icons
|
||||
|
||||
@@ -31,7 +31,13 @@ If you can't find the icon you're looking for in Homarr's built-in picker, we st
|
||||
|
||||
Icons that are high quality and printable ([SVG](https://wikipedia.org/wiki/Scalable_Vector_Graphics)) are marked with a red badge "SVG" at the top right.
|
||||
|
||||
You can clear the selected image with the clear button or the usual keyboard shortcut for deleting a line (Command+Backspace on macOS or Control+Backspace on other platforms). Clearing the field does not restore the previous image.
|
||||
|
||||
### Using external icons
|
||||
|
||||
If you don't find the icon you're looking for on Dashboard Icons or wish to use a different source, you can simply enter a URL that points to an icon.
|
||||
Direct HTTP(S) image URLs are used as entered instead of being searched in the built-in repositories. It is important that the URL directly points to the file without any advertisements or elements around it.
|
||||
Direct HTTP(S) image URLs are used as entered instead of being searched in the built-in repositories. The picker previews the image and shows whether it loaded successfully. It is important that the URL directly points to the file without any advertisements or elements around it.
|
||||
|
||||
### Uploading a local image
|
||||
|
||||
Users with media upload permission can select **Upload image** next to the field. After the upload finishes, the new local image is selected immediately and is available at the top of future searches.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"label": "Community",
|
||||
"position": 5,
|
||||
"position": 6,
|
||||
"collapsed": true,
|
||||
"link": {
|
||||
"type": "generated-index"
|
||||
|
||||
@@ -6,18 +6,10 @@ tags:
|
||||
- Configuration
|
||||
---
|
||||
|
||||
import imageStartFromScratchStepServerSettings from "./img/onboarding-from-scratch-step-server-settings.png";
|
||||
import imageStartFromScratchStepAdminUser from "./img/onboarding-from-scratch-step-admin-user.png";
|
||||
import imageStartFromScratchStepExternalAdminGroup from "./img/onboarding-from-scratch-step-external-admin-group.png";
|
||||
import 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 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";
|
||||
import manageImportDecryptionToken from "./img/old-homarr-import-decryption-token.png";
|
||||
|
||||
:::tip
|
||||
|
||||
@@ -29,117 +21,50 @@ Congratulations on installing Homarr. Now it's time to get yourself familiar wit
|
||||
|
||||
## Onboarding
|
||||
|
||||
The onboard will show the first time you start Homarr.
|
||||
This process will help you with your first steps and adding some initial data, such as the administrator account.
|
||||
Onboarding appears the first time Homarr starts. Its goal is to create a useful responsive board, not merely an
|
||||
administrator account. The setup uses one guided studio, so you can keep the safe defaults and finish quickly or review
|
||||
every detected service before anything is added to the board.
|
||||
|
||||
:::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 restarting 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.
|
||||
|
||||
:::
|
||||
|
||||
You can either import a previous dashboard using the ZIP archive or start from scratch:
|
||||
### Restore or start fresh
|
||||
|
||||

|
||||
SQLite installations can restore a complete Homarr database backup from the welcome screen. This restores current
|
||||
boards, people, apps, integrations, and settings. The pre-1.0 ZIP importer is no longer included.
|
||||
|
||||
You may refresh anytime and continue from a different device - the onboarding is saved immediately on the server for your convenience.
|
||||
For a new installation, Homarr first creates either a credentials administrator or an administrator group matching your
|
||||
LDAP/OIDC provider. The browser that starts setup claims the first-run session; refreshes are safe, while another browser
|
||||
cannot change credentials or integrations during that setup.
|
||||
|
||||
### Starting from scratch
|
||||
### Setup studio
|
||||
|
||||
Depending on your auth provider configuration you will either first see the admin user creation form or the external group creation form.
|
||||
If the credentials provider was selected, you'll be prompted to create an admin user for Homarr.
|
||||
This user cannot be deleted and has full control over Homarr and all of its boards.
|
||||
You must choose a username and password (both can later be changed).
|
||||
We enforce strict password requirements when you enter your password to protect your instance if you accidentally
|
||||
expose it to the internet.
|
||||
The studio keeps related choices together:
|
||||
|
||||
<img src={imageStartFromScratchStepAdminUser} alt={"Screenshot form to create admin user"} width={300} />
|
||||
1. **Essentials** sets the default language, color scheme, analytics preference, and the usual host, IP address, domain, or reverse-proxy path used by your services. URL suggestions can use separate ports, subdomains, or one path per service.
|
||||
2. **Discover** checks the actual database, Docker-compatible sockets, Kubernetes configuration, Workshop, and Assistant capabilities. Unavailable sources never block manual setup.
|
||||
3. **Connect** shows detected integrations and apps first, identifying whether each app came from an explicit [`homarr.*` or Homepage Docker label](/docs/integrations/docker-labels) or image matching. Select several integrations and apps at once, review every URL, and use the service-specific link to open its API-key settings when one is available. If a container has no published port or URL label, add its HTTP or HTTPS address before importing it.
|
||||
4. **Board** previews the Base layout, colors, widget radius, sidebars, selected apps, and widgets inferred from integrations or Docker labels. Choose 8 columns for a focused board, 10 for a balanced board, 12 for a wide board, or use the slider for a custom width up to 24 columns. Homarr still creates and reflows the protected Mobile layout automatically. A fast setup adds the integration-free Clock, Weather, and Bookmarks starter widgets.
|
||||
5. **Extend** explains and optionally configures Homarr Assistant, the community Workshop, and the separate Homarr MCP endpoint.
|
||||
6. **Review** tests the selected connections, then saves integrations, apps, widgets, settings, and the exact first board together. If any part fails, the database changes are rolled back and your form stays available to correct.
|
||||
|
||||
If you selected external providers (such as LDAP or OIDC), you'll be prompted to create an admin group.
|
||||
This group will be used to synchronize users from your provider to Homarr.
|
||||
When a user logs in through the provider, any of their groups that match groups in Homarr will be assigned to the user.
|
||||
The name of the group must match exactly the name of the group in the provider.
|
||||
Docker discovery reports each configured host separately. Results from reachable hosts remain selectable if another socket
|
||||
is unavailable, which is useful for remote Docker, rootless Docker, Synology, and mixed deployments. On Kubernetes or
|
||||
bare-metal installations without a Docker socket, enter services manually and use the same integration catalog.
|
||||
|
||||
<img
|
||||
src={imageStartFromScratchStepExternalAdminGroup}
|
||||
alt={"Screenshot form to create external admin group"}
|
||||
width={300}
|
||||
/>
|
||||
After setup, **Open my board** takes you directly to the configured board. The in-product tour then introduces edit mode,
|
||||
responsive layouts, widgets, and search without delaying completion.
|
||||
|
||||
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.
|
||||
|
||||
<img
|
||||
src={imageStartFromScratchStepServerSettings}
|
||||
alt={"Screenshot of analytics and indexing settings in the onboarding"}
|
||||
width={300}
|
||||
/>
|
||||
|
||||
### Setting up integrations
|
||||
|
||||
After configuring server settings, the onboarding guides you through setting up your integrations.
|
||||
|
||||
#### Docker service discovery
|
||||
|
||||
If Docker is available (socket mounted), Homarr automatically detects running containers and matches them to known integration types.
|
||||
Matching integrations are pre-selected in the integration list, saving you from manually picking each one.
|
||||
|
||||
#### URL templating
|
||||
|
||||
You can define a base URL template to auto-fill integration URLs in bulk.
|
||||
Two modes are supported:
|
||||
|
||||
- **Subdomain mode** - e.g. `{app}.example.com` where `{app}` is replaced with the integration name
|
||||
- **Port mode** - e.g. `192.168.1.1:{port}` where `{port}` is replaced with the integration's default port
|
||||
|
||||
#### API key helper links
|
||||
|
||||
For integrations that require API keys (such as Sonarr, Radarr, SABnzbd, or Seerr), a helper link opens the relevant settings page in the target application so you can quickly copy your key.
|
||||
|
||||
#### Creating apps from Docker containers
|
||||
|
||||
You can optionally auto-create bookmark apps for all discovered Docker containers, making them immediately available on your board.
|
||||
|
||||
:::note
|
||||
|
||||
Release providers and Search.ch are configured directly in their widgets instead of the integrations page.
|
||||
|
||||
:::
|
||||
|
||||
After this step, you finished the onboarding.
|
||||
The onboarding can no longer be started again.
|
||||
From here you can click on one of the provided links to continue your journey.
|
||||
We recommend you to continue with the section [creating your first board on this page](#creating-your-first-board).
|
||||
|
||||
<img src={imageStartFromScratchStepFinish} alt={"Screenshot of the finish onboarding process with links"} width={300} />
|
||||
|
||||
### Importing a ZIP from version before 1.0.0
|
||||
|
||||
This is recommended when you used Homarr before and want to re-import your data.
|
||||
It is important that you first migrate correctly using [the list of breaking changes for 1.0](/blog/2024/09/23/version-1.0).
|
||||
|
||||
<img src={manageExportOldHomarr} alt="Export from old Homarr" style={{ maxWidth: 500 }} />
|
||||
|
||||
**How to export and import data**
|
||||
|
||||
1. Navigate to your old Homarr, open your management pages, open the tools submenu and open `Migrate to 1.0`.
|
||||
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`
|
||||
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 }} />
|
||||
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.
|
||||
|
||||
## Creating your first board
|
||||
## Creating another board
|
||||
|
||||
In Homarr, you organize your apps and widgets on boards.
|
||||
Boards use fixed-size tiles so the same widget keeps the same proportions on every screen.
|
||||
Drag, resize, and section controls are available in edit mode.
|
||||
Let's create your first board by navigating here: `Manage` > `Boards` > `New board`:
|
||||
Onboarding already created the first board. To create another one, navigate to `Manage` > `Boards` > `New board`:
|
||||

|
||||
|
||||
A board must always have a name and can either be public or private.
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
title: "Docker labels"
|
||||
description: "Discover apps, integrations, and widgets from container labels during Homarr onboarding."
|
||||
---
|
||||
|
||||
# Docker label discovery
|
||||
|
||||
During onboarding, Homarr scans running containers on every reachable
|
||||
[Docker-compatible host](/docs/integrations/docker). Labels let you provide exact app metadata instead of relying on
|
||||
image-name matching. You can review and deselect every result before Homarr creates anything.
|
||||
|
||||
A container with valid Homarr labels takes priority over image-based discovery. Containers without valid labels can
|
||||
still be suggested from their image and published ports.
|
||||
|
||||
## Homarr labels
|
||||
|
||||
`homarr.name` and `homarr.href` are required for label discovery. All other labels are optional.
|
||||
|
||||
| Label | Purpose |
|
||||
| -------------------- | -------------------------------------------------------------------------- |
|
||||
| `homarr.name` | App or integration display name |
|
||||
| `homarr.href` | Address opened by the app and used as the suggested integration address |
|
||||
| `homarr.group` | Container section that receives the selected app and compatible widget |
|
||||
| `homarr.icon` | App icon URL |
|
||||
| `homarr.description` | App description |
|
||||
| `homarr.ping` | Address used for app status checks |
|
||||
| `homarr.id` | Stable source identifier; defaults to the Docker container ID |
|
||||
| `homarr.board` | Intended board name; see [Board targeting](#board-targeting) |
|
||||
| `homarr.integration` | Homarr integration kind, for example `sonarr` |
|
||||
| `homarr.widget` | Homarr widget kind to add when it supports the selected integration |
|
||||
| `homarr.hide` | Excludes the container when this label is present, regardless of its value |
|
||||
|
||||
Unknown integration and widget kinds are ignored. Do not put API keys, passwords, or other secrets in labels. Enter
|
||||
credentials in onboarding, where Homarr stores them using its normal encrypted integration-secret flow.
|
||||
|
||||
## Compose example
|
||||
|
||||
```yaml
|
||||
services:
|
||||
sonarr:
|
||||
image: lscr.io/linuxserver/sonarr:latest
|
||||
labels:
|
||||
homarr.name: "Sonarr"
|
||||
homarr.href: "http://server.local:8989"
|
||||
homarr.group: "Media"
|
||||
homarr.icon: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/sonarr.svg"
|
||||
homarr.description: "TV library"
|
||||
homarr.ping: "http://server.local:8989"
|
||||
homarr.integration: "sonarr"
|
||||
homarr.widget: "calendar"
|
||||
```
|
||||
|
||||
Use an address that is reachable where it is needed. A Docker service name can work for server-side integration
|
||||
requests, while an app link opened in a browser usually needs a LAN hostname, IP address, or reverse-proxy address.
|
||||
Onboarding accepts these self-hosted address formats directly and only requires the field to be non-empty.
|
||||
|
||||
## Homepage label fallback
|
||||
|
||||
When `homarr.name` is absent, onboarding also understands these Homepage labels:
|
||||
|
||||
- `homepage.name`
|
||||
- `homepage.href`
|
||||
- `homepage.group`
|
||||
- `homepage.icon`
|
||||
- `homepage.description`
|
||||
|
||||
`homepage.name` and `homepage.href` are required for the fallback. Homarr does not combine partial Homarr metadata with
|
||||
Homepage metadata: once `homarr.name` is present, use `homarr.*` labels for the remaining fields too.
|
||||
|
||||
## What onboarding creates
|
||||
|
||||
In **Connect**, label-discovered apps and integrations appear before image-based suggestions. You can import multiple
|
||||
services in one pass and edit every suggested address. For image-matched apps, Homarr uses the detected service type
|
||||
when building host, subdomain, or reverse-proxy path suggestions.
|
||||
|
||||
When setup completes, Homarr rechecks the selected container sources and then:
|
||||
|
||||
- creates an app with its name, address, icon, description, and ping address;
|
||||
- creates the requested integration and links its app after you provide required credentials;
|
||||
- places grouped apps in a board container named by `homarr.group`;
|
||||
- places ungrouped apps in the main board area;
|
||||
- adds `homarr.widget` only when the widget kind is valid and compatible with the selected integration;
|
||||
- keeps image-based discovery available for unlabeled containers.
|
||||
|
||||
If a selected container disappears, targets another board, or requests an incompatible widget, setup finishes with a
|
||||
warning instead of creating an invalid board item.
|
||||
|
||||
## Board targeting
|
||||
|
||||
On a fresh installation with one seeded `dashboard` board, one unique selected `homarr.board` value can name that first
|
||||
board. Otherwise, the label must match the board selected in onboarding. Services labeled for another board are skipped
|
||||
and reported in the completion warning.
|
||||
|
||||
Onboarding configures one board at a time. Run normal board management after setup if you want to distribute services
|
||||
across several boards.
|
||||
@@ -122,9 +122,77 @@ services:
|
||||
DOCKER_SOCKET_PATHS: /var/run/docker.sock,/var/run/podman.sock
|
||||
```
|
||||
|
||||
For named endpoints, transport security, or least-privilege actions, prefer `DOCKER_ENDPOINTS`. It is a JSON array where every endpoint declares its identity, transport, and capabilities:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
homarr:
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./docker-ca.pem:/run/secrets/docker-ca.pem:ro
|
||||
environment:
|
||||
DOCKER_ENDPOINTS: >-
|
||||
[
|
||||
{
|
||||
"id": "local",
|
||||
"name": "Local Docker",
|
||||
"kind": "docker",
|
||||
"transport": { "type": "socket", "path": "/var/run/docker.sock" },
|
||||
"capabilities": ["inventory", "logs", "lifecycle", "remove"]
|
||||
},
|
||||
{
|
||||
"id": "remote-read-only",
|
||||
"name": "Production inventory",
|
||||
"kind": "docker",
|
||||
"transport": {
|
||||
"type": "tls",
|
||||
"host": "docker.example.com",
|
||||
"port": 2376,
|
||||
"caPath": "/run/secrets/docker-ca.pem"
|
||||
},
|
||||
"capabilities": ["inventory", "logs"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
The stable `id` keeps containers from different endpoints distinct. `name` is shown in the interface, and `kind` can be `docker` or `podman`. Every endpoint must include the `inventory` capability; optionally add `logs`, `lifecycle`, and `remove`. Homarr hides or disables actions the endpoint does not allow and also enforces the same capabilities on the server.
|
||||
|
||||
Supported transports are:
|
||||
|
||||
- `socket`: an absolute in-container `path`
|
||||
- `tls`: `host`, `port`, and an absolute `caPath`; add both `certPath` and `keyPath` for mutual TLS
|
||||
- `tcp`: `host`, `port`, and the explicit acknowledgement `allowInsecure: true`
|
||||
|
||||
:::warning[Plaintext TCP]
|
||||
Plaintext Docker TCP can grant unauthenticated control of the Docker host. Prefer a local socket, a least-privilege socket proxy, or TLS. The structured configuration refuses TCP unless `allowInsecure` is explicitly set to `true`.
|
||||
:::
|
||||
|
||||
When `DOCKER_ENDPOINTS` is set, it takes precedence over the legacy `DOCKER_SOCKET_PATHS`, `DOCKER_HOSTNAMES`, and `DOCKER_PORTS` variables.
|
||||
|
||||
### Managing your Docker containers within Homarr
|
||||
|
||||
Homarr allows you to interact with Docker containers running on your system.
|
||||
The **Service setup** section compares the visible containers with your existing Homarr apps and integrations. It keeps every service distinct by Docker endpoint and container ID, including multiple containers that use the same integration type.
|
||||
|
||||
Homarr can show a service as:
|
||||
|
||||
- **Ready to connect** when its image or name matches a supported integration
|
||||
- **New service** when it can be added as an app but no integration is recognized
|
||||
- **Already represented** or **Linked** when an app or integration already matches
|
||||
- **Address changed** when a service appears to match an integration but its reachable address has changed
|
||||
|
||||
Suggested addresses are ranked and explain where they came from. A published address or Docker endpoint host is normally usable from a browser; a container-name address is server-only and requires Homarr to share the container network. Wildcard addresses such as `0.0.0.0` are never offered as browser destinations. You can edit every suggestion before continuing.
|
||||
|
||||
:::info[Review before changing]
|
||||
Service discovery is read-only. Homarr never creates or changes an app or integration automatically; use the action on an individual suggestion to review and confirm its setup.
|
||||
:::
|
||||
|
||||
Use the inbox filters to focus on services that need attention or those already represented in Homarr. Refresh runs discovery again. Dismissed suggestions are stored in the current browser using their endpoint-qualified service identity and can be restored from the same panel. If multiple existing items could match, Homarr marks the result as ambiguous and does not choose one automatically.
|
||||
|
||||
The health badges are an evidence-based projection across Docker, integration configuration, app representation, and persisted widget links. Authentication, API request success, and widget query execution are labeled **Not observed** because Homarr does not currently retain trustworthy last-run results for these layers. Use the suggested integration test or board action to diagnose them live; **Not observed** is never presented as healthy.
|
||||
|
||||
If one Docker endpoint is unavailable, Homarr reports that endpoint while continuing to show services from healthy endpoints. An unavailable endpoint is not treated as an empty Docker installation.
|
||||
|
||||
Each container row has an action menu with the following options:
|
||||
|
||||
- **View logs** - opens a dedicated logs page with a live terminal
|
||||
@@ -160,11 +228,13 @@ They are then available in the app list and can be used as normally added apps.
|
||||
|
||||

|
||||
|
||||
### Configuration
|
||||
### Container labels
|
||||
|
||||
Currently Homarr supports one label for configuration of the containers:
|
||||
`homarr.hide` excludes a container from the Docker Containers widget, tools page, and onboarding discovery whenever the
|
||||
label is present.
|
||||
|
||||
- **homarr.hide:** If set to any value, the container will be hidden from the Docker Containers widget and tools page.
|
||||
During first-run onboarding, Homarr can also discover apps, integrations, groups, and compatible widgets from
|
||||
`homarr.*` labels, with Homepage labels as a fallback. See the complete [Docker label reference](/docs/integrations/docker-labels).
|
||||
|
||||
### Security
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ import { kubernetesIntegration } from ".";
|
||||
|
||||
Homarr provides a user-friendly Kubernetes dashboard that offers an overview of your cluster’s resources in a clear and structured interface. It allows users to view key Kubernetes objects, including `Pods`, `Services`, `Ingresses`, `Nodes`, `ConfigMaps`, `Secrets`, `Namespaces`, and `Volumes`, as well as basic metrics when the Kubernetes Metrics Server is enabled.
|
||||
|
||||
Docker service discovery does not convert Kubernetes resources into Homarr apps or integrations. Kubernetes remains a separate, read-only operational view, so a cluster resource is never changed or linked automatically.
|
||||
|
||||
:::warning[Read only]
|
||||
This integration is read-only, meaning Homarr does not modify or manage cluster resources but serves as an informational tool to enhance visibility into your Kubernetes environment.
|
||||
:::
|
||||
@@ -25,7 +27,7 @@ This integration is read-only, meaning Homarr does not modify or manage cluster
|
||||
Before deploying Homarr in Kubernetes, ensure the following prerequisites are met:
|
||||
|
||||
- Kubernetes (Version >= 1.24.0)
|
||||
- Metrics Server ([Install from here](https://github.com/kubernetes-sigs/metrics-server))
|
||||
- Metrics Server ([install from here](https://github.com/kubernetes-sigs/metrics-server)) is optional. Without it, Homarr still shows cluster inventory, node status, and reserved capacity, and identifies live CPU and memory usage as unavailable.
|
||||
- RBAC Enabled (Required for Homarr Helm chart)
|
||||
|
||||
### Configuring RBAC in Helm Chart
|
||||
@@ -45,6 +47,25 @@ Enabling RBAC in the Helm chart creates the necessary Kubernetes Role-Based Acce
|
||||
- ClusterRole & ClusterRoleBinding: Extends the same permissions cluster-wide for additional flexibility.
|
||||
:::
|
||||
|
||||
### Multiple Kubernetes contexts
|
||||
|
||||
When Homarr runs outside a cluster, set `KUBECONFIG` explicitly so Homarr loads the contexts from the referenced kubeconfig file or files. Mount the kubeconfig read-only and set the standard `KUBECONFIG` environment variable to its path inside the Homarr container:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
homarr:
|
||||
volumes:
|
||||
- ./kubeconfig:/app/config/kubeconfig:ro
|
||||
environment:
|
||||
KUBECONFIG: /app/config/kubeconfig
|
||||
```
|
||||
|
||||
The Kubernetes management pages include a context selector. The selected context is stored in the page URL, so each React Query cache entry and every API request remains context-specific. Resources with the same namespace and name in two contexts therefore remain separate.
|
||||
|
||||
Context status is checked independently. If one context is unavailable, the selector still lists healthy contexts. A context is shown as **Metrics unavailable** when its Kubernetes API is reachable but Metrics Server is not; inventory remains available without live CPU and memory usage.
|
||||
|
||||
When Homarr runs inside Kubernetes without `KUBECONFIG`, it keeps using the in-cluster service account as the single default context.
|
||||
|
||||
### Homarr Dashboard Overview
|
||||
|
||||
Once deployed, Homarr provides a dashboard displaying Kubernetes metrics and resource information. The main page includes:
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
title: "What's Up Docker"
|
||||
description: "What's Up Docker (WUD) watches your Docker containers and reports which ones have newer image versions available."
|
||||
hide_title: true
|
||||
---
|
||||
|
||||
import { IntegrationHeader } from "@site/src/components/integrations/header";
|
||||
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
|
||||
import { AddingIntegration } from "@site/src/components/integrations/adding";
|
||||
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
|
||||
import { wudIntegration } from ".";
|
||||
import { wudWidget } from "@site/docs/widgets/whats-up-docker";
|
||||
|
||||
<IntegrationHeader integration={wudIntegration} categories={["Health Monitoring"]} />
|
||||
|
||||
The What's Up Docker integration lets Homarr connect to your [WUD](https://github.com/getwud/wud) instance so you can see how many of your watched containers have image updates available directly from your dashboard.
|
||||
|
||||
### Widgets & Capabilities
|
||||
|
||||
<IntegrationCapabilites items={[{ widget: wudWidget }]} />
|
||||
|
||||
### Adding the integration
|
||||
|
||||
<AddingIntegration />
|
||||
|
||||
:::info URL configuration
|
||||
Use the root URL of your WUD instance, for example `https://wud.example.com`. Homarr calls the `/api/app` and `/api/containers` endpoints under that URL automatically.
|
||||
:::
|
||||
|
||||
### Secrets
|
||||
|
||||
WUD does not require authentication by default. If your instance is protected with [Basic Authentication](https://github.com/getwud/wud/blob/main/docs/configuration/authentications/basic/README.md), you can provide a username and password.
|
||||
|
||||
<IntegrationSecrets
|
||||
secrets={[
|
||||
{
|
||||
credentials: [],
|
||||
steps: [],
|
||||
},
|
||||
{
|
||||
credentials: ["username", "password"],
|
||||
steps: [
|
||||
"Open your WUD instance's configuration and configure a WUD_AUTH_BASIC_* user as described in the WUD documentation",
|
||||
"Enter that username and password in Homarr — Basic authentication is handled automatically",
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
@@ -0,0 +1,9 @@
|
||||
import { IntegrationDefinition } from "@site/src/types";
|
||||
|
||||
export const wudIntegration: IntegrationDefinition = {
|
||||
name: "What's Up Docker",
|
||||
description:
|
||||
"What's Up Docker (WUD) watches your Docker containers and reports which ones have newer image versions available.",
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/whats-up-docker.svg",
|
||||
path: "../../integrations/whats-up-docker",
|
||||
};
|
||||
@@ -82,8 +82,34 @@ The OpenAPI and MCP APIs include board management endpoints for common automatio
|
||||
- Set desktop and mobile home boards for the current user
|
||||
- Configure global board defaults such as public home boards and status behavior
|
||||
|
||||
Board layout responses identify each layout with a `mobile`, `base`, or `custom` role. Mobile and Base are protected. Layout-saving clients must preserve existing roles, keep Mobile at breakpoint `0`, keep Base as the highest breakpoint, and use unique custom breakpoints between them. The layout save operation returns the canonical saved layouts, including server-generated IDs for new custom layouts. A non-Base layout can be regenerated from Base with the protected reset operation.
|
||||
|
||||
Use the interactive API reference to inspect the exact request body for each endpoint under the `boards` and `settings` tags.
|
||||
|
||||
## Docker action payload migration
|
||||
|
||||
Docker lifecycle and removal actions now identify each container with both its Docker endpoint and container ID. This is a
|
||||
breaking change for tRPC and MCP clients that previously sent an `ids` array to the start, stop, restart, or remove action.
|
||||
|
||||
Replace the old payload:
|
||||
|
||||
```json
|
||||
{ "ids": ["container-id"] }
|
||||
```
|
||||
|
||||
with endpoint-qualified targets:
|
||||
|
||||
```json
|
||||
{
|
||||
"targets": [{ "endpointId": "local", "id": "container-id" }]
|
||||
}
|
||||
```
|
||||
|
||||
Read `endpointId` and `id` from `docker.getContainers` (MCP tool `docker_getContainers`) immediately before the action.
|
||||
Pass those pairs unchanged to `docker.startAll`, `docker.stopAll`, `docker.restartAll`, or `docker.removeAll`; their MCP tool
|
||||
names use underscores, such as `docker_restartAll`. Endpoint qualification prevents identical container IDs on different
|
||||
Docker or Podman endpoints from targeting the wrong service.
|
||||
|
||||
## Partial integration failures
|
||||
|
||||
Multi-integration widget queries keep data from integrations that remain available. Calendar and media-server results include
|
||||
|
||||
@@ -28,7 +28,7 @@ The name must be at least one character long and can be chosen freely.
|
||||
### Icon URL
|
||||
|
||||
This is the URL to the icon that will be displayed on the app widget.
|
||||
You can paste a direct HTTP(S) URL to an image, search the included icon repositories by name, or upload a local file through the upload button. Direct image URLs are used as entered and are not searched in the icon repositories.
|
||||
You can paste a direct HTTP(S) URL to an image, search the included icon repositories by name, or upload a local file through the upload button. The app name is pre-filled as the initial library search when no image is selected. Local images appear first, followed by SVG icons and other formats. Direct image URLs are previewed, used as entered, and are not searched in the icon repositories.
|
||||
|
||||

|
||||
|
||||
@@ -51,6 +51,6 @@ Note that the ping URL only accepts `http://` and `https://` URLs — custom URI
|
||||
|
||||
## List of apps
|
||||
|
||||
On the apps management page you find all apps that exist in your Homarr instance. You can edit and delete them.
|
||||
On the apps management page you find all apps that exist in your Homarr instance. Search by name or URL, open an app directly, or use its row actions to edit and delete it. A filtered search can be cleared without leaving the page.
|
||||
|
||||

|
||||
Any signed-in user can view this list. Creating, editing, and deleting apps requires the corresponding app permission; board permissions are not required.
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
description: Configure and use Homarr Assistant.
|
||||
tags:
|
||||
- Assistant
|
||||
- AI
|
||||
- Administration
|
||||
---
|
||||
|
||||
# Assistant
|
||||
|
||||
Homarr Assistant can answer questions and manage Homarr for you. It uses the signed-in user's permissions and asks for
|
||||
approval before making changes.
|
||||
|
||||

|
||||
|
||||
## Set it up
|
||||
|
||||
Open **Management → Assistant**.
|
||||
|
||||

|
||||
|
||||
1. Select a provider and check its API URL.
|
||||
2. Enter an API key when required, then save the connection.
|
||||
3. Refresh the model list and select a model.
|
||||
4. Enable the assistant and save.
|
||||
|
||||
Choose a provider preset or use **Custom endpoint** with an OpenAI-compatible Chat Completions API. The
|
||||
[Homarr provider](../workshop/homarr-provider) is the default and needs no administrator API key. The Homarr team pays
|
||||
for a limited number of Assistant uses for each user every day; users sign in to the Community Workshop to use them.
|
||||
|
||||
:::info Local model servers
|
||||
|
||||
Inside a container, `localhost` means the Homarr container. Use the model server's container name, local DNS name, or
|
||||
host gateway address.
|
||||
|
||||
:::
|
||||
|
||||
## Use it
|
||||
|
||||
- Select **Assistant** in the user menu or press `Shift + A`.
|
||||
- Ask from `Cmd/Ctrl + K`.
|
||||
- Add the [Assistant widget](../widgets/assistant) to a board.
|
||||
|
||||
The assistant can work with boards, apps, integrations, widgets, Custom Widgets, and connected services. Use `@` to
|
||||
add Homarr items as context, `/` for shortcuts, and the paperclip for supported files.
|
||||
|
||||
Each request includes the user's name, current page and board, home board, server time, browser time zone, and a small
|
||||
summary of accessible Homarr resources. Live status is fetched only when needed.
|
||||
|
||||
## Permissions and privacy
|
||||
|
||||
- Read-only actions can run automatically. Changes require approval.
|
||||
- Homarr permissions apply to every tool call and mentioned item.
|
||||
- API keys and custom headers are encrypted and never returned to the browser.
|
||||
- Conversations are stored per user and can be exported or deleted.
|
||||
|
||||
Prompts, attachments, tool definitions, and tool results are sent to the selected model provider. Review that
|
||||
provider's privacy policy before enabling the assistant.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **No models found:** check the base URL, model discovery path, and API key. You can enter a model ID manually.
|
||||
- **Assistant is unavailable:** enable it and save the configuration.
|
||||
- **Tool calls stop:** select a model that supports streaming and tool calls.
|
||||
- **Local endpoint is unreachable:** check container networking and the server's listen address.
|
||||
|
||||
See [MCP](./mcp) for external AI clients and [Custom Widget agent authoring](./custom-widgets/agent-authoring) for the
|
||||
widget authoring workflow.
|
||||
@@ -22,6 +22,8 @@ Navigate to `Management` → `Tools` → `Backup`. You must have **admin** permi
|
||||
|
||||
## Exporting a Backup
|
||||
|
||||

|
||||
|
||||
Click the **Export** button in the export card. You'll be prompted to type `I understand` to confirm. Once confirmed, a ZIP file downloads containing:
|
||||
|
||||
| File | Description |
|
||||
@@ -33,6 +35,8 @@ The backup contains the complete database. It includes Custom Widgets and all ot
|
||||
|
||||
## Restoring a Backup
|
||||
|
||||

|
||||
|
||||
### 1. Upload
|
||||
|
||||
Drag and drop or select a `.zip` backup file. The import card validates that the file contains a valid `db.sqlite` and metadata.
|
||||
@@ -49,6 +53,9 @@ Before restoring, Homarr uses **sql.js WASM** to analyze the backup directly in
|
||||
|
||||
Type `I understand` to acknowledge that restoring will replace your current database. This cannot be undone.
|
||||
|
||||
During first-run onboarding, the database is still empty, so the restore preview goes directly to **Restore Database**
|
||||
without the extra typed confirmation. After Homarr restarts, onboarding opens the restored default home board.
|
||||
|
||||
### 4. Restore
|
||||
|
||||
The backup ZIP is sent to the server, validated, and applied. After a successful restore:
|
||||
|
||||
@@ -20,6 +20,8 @@ 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.
|
||||
The management page includes a compact structural preview of each board's Base layout so you can recognize it before
|
||||
opening it. Containers include their visible widgets and app icons instead of appearing empty.
|
||||
|
||||
Names of boards must be unique because this will be used in the URL to load the desired board.
|
||||
|
||||
@@ -90,11 +92,12 @@ Homarr will fall back to the Homarr logo if nothing is set.
|
||||
|
||||
### Layout
|
||||
|
||||
In this section you can configure the different responsive layouts.
|
||||
Homarr selects a layout from the screen width, then scales its complete fixed-size canvas uniformly.
|
||||
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.
|
||||
|
||||

|
||||
|
||||
Every board has two protected layouts:
|
||||
|
||||
Every `1 × 1` tile has a `200 × 200` logical-pixel content area.
|
||||
The `24` logical-pixel gap between tiles is separate from that size.
|
||||
A `2 × 1` tile therefore contains two fixed cell widths plus the gap between them.
|
||||
@@ -103,28 +106,31 @@ When fitting below 100%, Homarr keeps widget typography and control density at t
|
||||
On larger canvases, the complete board grows together like browser zoom.
|
||||
The complete canvas always fits the available width and never adds horizontal board scrolling.
|
||||
|
||||
The position and size of every item are independent in each responsive layout.
|
||||
Moving or resizing an item in one layout does not change any other layout.
|
||||
When adding a new layout, its initial positions and sizes are generated from the column count.
|
||||
New items use their widget's default grid size, while a new Container starts at a useful `4 × 3` size when the layout has enough columns.
|
||||
Both are placed in the first available position.
|
||||
- **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 can be changed, but they cannot be removed. Mobile has no fixed rails; rail configuration is available only on Base and custom layouts. 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.
|
||||
|
||||
Use **Reset from Base** on Mobile or a custom layout to replace all of its positions and sizes with a newly generated version of Base. The layout's name, breakpoint, and column count are kept.
|
||||
|
||||
The canvas is at least one window tall, even when it is empty.
|
||||
Its left and right edges are hard drag boundaries, while its bottom edge extends automatically as a tile is dragged or resized downward.
|
||||
Nested Containers keep their own resizable bounds.
|
||||
New items use their widget's default size and new Containers use a useful `4 × 3` size when space allows.
|
||||
|
||||
#### Name
|
||||
|
||||
The name of the layout can be chosen freely. It is only used for management and will not be visible on the board.
|
||||
The name of the layout can be chosen freely. It is only used for management and is not displayed on the board.
|
||||
|
||||
#### Column count
|
||||
|
||||
Total number of columns for this layout.
|
||||
When changed, Homarr moves items into the next available positions without overlap.
|
||||
The total number of columns in the layout. Changing it projects the saved positions and sizes into the new grid width without overlap.
|
||||
|
||||
#### Fixed rails
|
||||
|
||||
Each responsive layout can enable a fixed left rail, a fixed right rail, or both.
|
||||
Base and custom responsive layouts can enable a fixed left rail, a fixed right rail, or both. Mobile has no fixed rails.
|
||||
Set a rail width to zero to disable it for that layout.
|
||||
Choose a width from one to three columns with the three-step slider.
|
||||
The preview shows how much space remains for the main canvas.
|
||||
@@ -134,13 +140,12 @@ For example, a 12-column layout with a two-column left rail leaves ten columns f
|
||||
Enabling or widening a rail reflows canvas items into the remaining columns.
|
||||
Disabling a rail moves its contents back into the next available canvas positions.
|
||||
|
||||
Rails fill the available window height and remain fixed while the page scrolls.
|
||||
Rails fill the available window height and remain fixed while the page scrolls. They do not grow or scroll independently.
|
||||
Apps, widgets, and Containers inside them still use the same fixed `200 × 200` cells and can be rearranged in edit mode, where each rail shows its name and boundary.
|
||||
|
||||
#### Breakpoint
|
||||
|
||||
Minimum width of the screen where the layout will be used.
|
||||
Once the minimum of the next layout is reached, the layout will change.
|
||||
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.
|
||||
|
||||
#### View and edit modes
|
||||
|
||||
@@ -154,6 +159,15 @@ The visible corner controls provide a 44-pixel resize target, and edge controls
|
||||
Resize handles stop at the tile's minimum allowed size while the pointer is still moving.
|
||||
Exit edit mode to save the resulting positions.
|
||||
|
||||
While edit mode is active, **Add board content** opens the same Universal Create surface as the global header action and
|
||||
Spotlight. It can add a widget, app, integration, Container, board, Workshop item, or Custom Widget when the
|
||||
current user and context allow it. Widget setup offers the main canvas, enabled rails, and existing Containers as
|
||||
destinations before the widget is created, so the common flow does not require a second move step.
|
||||
|
||||
The widget editor also lists its selected integrations. Users with full access can choose **Edit connection** to test,
|
||||
diagnose, and repair an integration without leaving the board. A successful repair refreshes the affected widget; the
|
||||
saved integration changes apply everywhere that connection is used.
|
||||
|
||||
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.
|
||||
@@ -165,6 +179,10 @@ The **Move / resize item** action in the edit menu remains available for precise
|
||||
|
||||
#### 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.
|
||||
|
||||
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} />
|
||||
@@ -240,6 +258,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.
|
||||
|
||||
[See this guide on how to work with custom CSS](/docs/advanced/styling/).
|
||||
|
||||
### Behaviour
|
||||
|
||||
@@ -17,7 +17,9 @@ Install the Agent Skill from the verified Homarr repository:
|
||||
npx skills add https://github.com/homarr-labs/homarr --skill homarr-custom-widget
|
||||
```
|
||||
|
||||
The portable skill gives concise authoring guidance. The Homarr MCP prompt gives connected agents a retrieval workflow: read the live schema, then fetch only the component documentation needed for the widget.
|
||||
The portable skill gives concise authoring guidance. For connected agents, Homarr returns the complete `SKILL.md` and
|
||||
all bundled skill references once in one lazy tool response. The Homarr MCP prompt then gives the agent a
|
||||
retrieval workflow: read the live schema and fetch only the component documentation needed for the widget.
|
||||
|
||||
The workbench's **Copy AI prompt** action is separate. It creates a self-contained prompt below 12,000 characters for an ordinary ChatGPT or Claude conversation. It includes the lean contract, bindings, recommended components, and two polished examples. It never tells an offline model to call MCP tools.
|
||||
|
||||
@@ -37,38 +39,55 @@ Repository code cannot give this attribution.
|
||||
|
||||
The authoring-prompt tool description contains one sentence. The prompt contains the workflow instructions and resource links.
|
||||
|
||||
`GET /api/custom-widgets/skill` and the MCP tool return the concise skill discovery payload:
|
||||
`GET /api/custom-widgets/skill` and `customWidget_getSkill` return a self-contained structured skill payload:
|
||||
|
||||
```ts
|
||||
{
|
||||
name: "homarr-custom-widget";
|
||||
version: string;
|
||||
skillMd: string;
|
||||
skillMd: string; // Complete SKILL.md
|
||||
references: Record<string, string>;
|
||||
skillsShUrl: string;
|
||||
sourceUrl: string;
|
||||
installCommand: string;
|
||||
}
|
||||
```
|
||||
|
||||
The full installed component catalog stays available through `homarr://custom-widgets/components` and per-component resources. It is deliberately not embedded in the skill or clipboard prompt.
|
||||
The `homarr://custom-widgets/skill` MCP resource returns the same `SKILL.md` and references once as bundled Markdown.
|
||||
|
||||
The component catalog is a compact index of names, categories, safety levels, shared prop names, policies, and example
|
||||
IDs. Per-component results contain only component-specific props and a reference to shared prop documentation. Use
|
||||
`customWidget_getSharedProps` once with only the shared prop names needed across the selected components. This avoids
|
||||
repeating hundreds of global props in every tool result. The catalog is deliberately not embedded in the skill or
|
||||
clipboard prompt; the skill response still contains the complete authoring, schema, runtime, and security references.
|
||||
|
||||
These interfaces do not return an archive URL or archive hash.
|
||||
|
||||
## Create a Custom Widget with MCP
|
||||
|
||||
1. Get the prompt, live schema, and component catalog.
|
||||
2. Read the external API documentation.
|
||||
3. Build one definition.
|
||||
4. Validate the definition without saving it.
|
||||
5. Create a preview session.
|
||||
6. Test the queries and actions.
|
||||
7. Open the preview URL with browser tools.
|
||||
8. Inspect the redacted journal and revise the local candidate until it is valid.
|
||||
9. Create the widget, including any source URLs and credentials the user already supplied.
|
||||
10. Otherwise, ask the user to configure the saved definition with the ten-minute Homarr source-setup URL.
|
||||
11. Create a final preview with the saved definition ID so Homarr loads its stored credentials, then test it again.
|
||||
12. Update the saved definition if the final verification finds a problem.
|
||||
13. Repeat the procedure for the next widget.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
12. Open its returned management path, and use a structured question when the user must choose whether to place it on a board.
|
||||
13. Otherwise, ask the user to configure the saved definition with the ten-minute Homarr source-setup URL.
|
||||
14. Create a final preview with the saved definition ID so Homarr loads its stored credentials, then test it again.
|
||||
15. Update the saved definition if the final verification finds a problem. `customWidget_update` also accepts
|
||||
`templateLines` for multiline JSX without resetting omitted options.
|
||||
16. Repeat the procedure for the next widget.
|
||||
|
||||
Successful validation returns a compact summary instead of echoing the full widget and template. Preview creation
|
||||
returns its session, links, and query checklist without repeating the submitted definition. Keep the candidate locally
|
||||
and reuse it for the next lifecycle step.
|
||||
|
||||
Each source-configuration URL applies to one definition or preview and one source. It is short-lived, single-use, and
|
||||
can only be opened and completed by an authenticated Homarr administrator. The administrator confirms the server URL,
|
||||
@@ -94,11 +113,51 @@ Homarr maintainers can run the same self-contained prompt that the workbench cop
|
||||
OPENROUTER_API_KEY=... pnpm --filter @homarr/custom-widgets test:ai
|
||||
```
|
||||
|
||||
The suite uses DeepSeek V4 Pro to author Pokédex, Portainer, football, Jellyfin, and Home Assistant widgets. Before DeepSeek V4 Flash judges a result, it must pass Homarr's real clipboard parser, canonical schema, and deterministic scenario checks for the documented API routes, authentication, bindings, actions, and essential runtime components. It saves every response, validated widget manifest, and detailed category scores under `packages/custom-widgets/.ai-evaluations/`.
|
||||
The manifest suite covers nine complex and public-API prompts. Its deterministic fixture-backed cases include a fake
|
||||
homelab health API, CoinMarketCap's keyless public API, Bored, Agify, and a multi-request Pokédex. Before the configured
|
||||
model judges a result, the candidate must pass Homarr's real clipboard parser, canonical schema, and deterministic
|
||||
checks for documented endpoints, authentication, request triggers, query bindings, response fields, and essential
|
||||
runtime components.
|
||||
|
||||
Each scenario stops as soon as it passes and has a hard limit of ten generation attempts. Override the models or
|
||||
per-scenario limit with `OPENROUTER_GENERATOR_MODEL`, `OPENROUTER_JUDGE_MODEL`, and
|
||||
To exercise the real Homarr Assistant lifecycle instead of accepting a returned JSON block, run:
|
||||
|
||||
```sh
|
||||
OPENROUTER_API_KEY=... pnpm --filter @homarr/custom-widgets test:assistant-ai
|
||||
```
|
||||
|
||||
The same suites can target the Homarr provider with a Workshop PocketBase token instead of an OpenRouter key:
|
||||
|
||||
```sh
|
||||
AI_PROVIDER_BASE_URL=https://homarr.dev/api/ai/v1 \
|
||||
AI_PROVIDER_API_KEY=<workshop-token> \
|
||||
AI_PROVIDER_MODEL=homarr/model \
|
||||
pnpm --filter @homarr/custom-widgets test:assistant-ai
|
||||
```
|
||||
|
||||
`AI_PROVIDER_JUDGE_MODEL` can select a separate judge when the configured endpoint offers one. The Homarr provider
|
||||
offers exactly one model, so generation and review both use `homarr/model`.
|
||||
|
||||
The assistant test exposes the same lazy skill, live schema, component catalog, targeted component references,
|
||||
validation, preview-query, and create-from-preview tools as the product. It refuses to save a definition that was not
|
||||
validated exactly or whose preview queries were not all tested. Multi-request fixtures return a different payload for
|
||||
each documented path and require manual-query parameters. Use `--case=pokedex`, `--case=coinmarketcap-keyless`,
|
||||
`--case=bored-activity`, or `--case=agify-name` for a focused run.
|
||||
|
||||
Both suites use the configured provider models for generation and harsh review. They save tool traces, validated
|
||||
manifests, raw reviews, and detailed category scores under
|
||||
`packages/custom-widgets/.ai-evaluations/`. These live tests are maintainer-invoked rather than CI-required because
|
||||
they consume a configured provider account and exercise a nondeterministic model. The repository's ordinary tests
|
||||
cover the lifecycle enforcement, fixture routing, score calculation, and failure behavior without network access.
|
||||
|
||||
Each scenario stops as soon as it passes and has a hard limit of ten generation attempts. Override the endpoint,
|
||||
models, or per-scenario limit with `AI_PROVIDER_BASE_URL`, `AI_PROVIDER_API_KEY`, `AI_PROVIDER_MODEL`,
|
||||
`AI_PROVIDER_JUDGE_MODEL`, and
|
||||
`CUSTOM_WIDGET_AI_MAX_LOOPS`. Generated artifacts are local and ignored by Git; promote a reviewed, validated result
|
||||
into the bundled-widget source deliberately.
|
||||
|
||||
The reference API check uses a real PokeAPI list/detail request and a read-only Portainer container query when `PORTAINER_BASE_URL` and `PORTAINER_API_KEY` are set. Use `pnpm --filter @homarr/custom-widgets test:reference-apis -- --pokeapi-only` for the public PokeAPI check alone. To exercise start, restart, and stop against a disposable container, also set `PORTAINER_TEST_CONTAINER_ID` and the explicit safeguard `PORTAINER_ALLOW_MUTATIONS=1`; the script restores the container's original running state.
|
||||
|
||||
Use `pnpm --filter @homarr/custom-widgets test:reference-apis -- --public-ai-apis-only` to verify the live Bored and
|
||||
CoinMarketCap response shapes used by the assistant fixtures. The same command also verifies Agify when
|
||||
`AGIFY_API_KEY` is configured; otherwise it reports the authenticated check as skipped without weakening the
|
||||
deterministic Agify fixture tests.
|
||||
@@ -25,10 +25,10 @@ Every option has a default. Supported controls include text, textarea, number, s
|
||||
|
||||
## ChatGPT or Claude
|
||||
|
||||
Enter the desired widget and optional API documentation URL, then select **Copy AI prompt**. The single self-contained prompt is below 12,000 characters, starts with your request, includes the lean contract and two polished examples, and never claims that an offline chat has MCP tools.
|
||||
Open **Create or fix with any AI**, enter the desired widget and optional API documentation URL, then select the copy action beside the accordion title. The single self-contained prompt is below 12,000 characters, starts with your request, includes the lean contract and two polished examples, and never claims that an offline chat has MCP tools.
|
||||
|
||||
The model must return one fenced `json` block containing the complete widget, including the JSX in its `template`
|
||||
field. Copy that single block and select **Paste AI response**. Homarr fills the visual builders and the JSX editor
|
||||
field. Copy that single block and select **Paste**. Homarr fills the visual builders and the JSX editor
|
||||
without any placeholder replacement or second paste.
|
||||
|
||||
Homarr shows all actionable schema or JSX errors inline. Enter credentials separately. The prompt deliberately
|
||||
|
||||
@@ -40,6 +40,12 @@ 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.
|
||||
|
||||
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.
|
||||
|
||||
<CustomWidgetCodeExample
|
||||
id="complete-widget-manifest"
|
||||
label="Complete manifest"
|
||||
@@ -90,15 +96,17 @@ while the replacement exists; deleting the v2 record surfaces the v1 entry as mi
|
||||
legacy deletion is intentionally unavailable during v2 stabilization. Credentials are never included in prompts,
|
||||
logs, or exports.
|
||||
|
||||
For administrators adding widgets while editing a board, the Custom Widgets section includes **Install from
|
||||
Workshop**. Non-administrators do not see the Custom Widget picker or Workshop actions. The Workshop and API source
|
||||
review open above the widget picker without closing it. After import, the new definition appears in the picker. Its tile
|
||||
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.
|
||||
|
||||
## Import and Workshop
|
||||
|
||||
Import review shows origins, authentication modes, network scopes, methods, actions, and permissions. Every source has a suggested URL, but private and loopback sources must be confirmed or replaced for the current Homarr installation. The same screen accepts API keys, tokens, usernames, and passwords; credentials remain optional at install time and missing values stay visible on the Custom Widgets page.
|
||||
The install flow always links to the corresponding public Workshop entry using `WORKSHOP_WEB_URL`. Import confirmation shows origins, authentication modes, network scopes, methods, actions, and permissions. Every source has a suggested URL, but private and loopback sources must be confirmed or replaced for the current Homarr installation. The same screen accepts API keys, tokens, usernames, and passwords; credentials remain optional at install time and missing values stay visible on the Custom Widgets page.
|
||||
|
||||
For example, installing **Tautulli Activity** asks for both the user's Tautulli URL and API key. A stable public source such as PokéAPI keeps its published URL without creating unnecessary setup work.
|
||||
|
||||
|
||||
@@ -13,9 +13,9 @@ Check [the available integrations](/docs/category/integrations) for further info
|
||||
|
||||
## Create an integration
|
||||
|
||||
To create an integration, you can choose the supported application from the dropdown at the top right:
|
||||
To create an integration, choose **New integration** and select the supported application from the searchable grid. The grid groups related services visually and shows which widgets each integration supports.
|
||||
|
||||

|
||||
The integrations management page uses a searchable list. Each row shows the integration type and URL, with the actions you have permission to use.
|
||||
|
||||
Next, you'll need to fill out the required fields to configure the integration:
|
||||
|
||||
@@ -59,6 +59,22 @@ It is not allowed to create an integration without passing the test.
|
||||
This is to ensure that you minimize the amount of broken applications on your Homarr instance.
|
||||
The test and create button will perform a network request to the application and authenticate if required.
|
||||
|
||||
When you connect a service from a board, Homarr keeps you in the same setup flow. After the connection succeeds, it
|
||||
confirms whether an application shortcut was linked and offers the widgets that support the new integration. Selecting a
|
||||
widget returns to its settings with the new integration already selected.
|
||||
|
||||
Users with full access can also test and update a selected integration from **Edit widget → Edit integration** on a
|
||||
board. Failed tests keep the editor open and show the same connection diagnostics as the management page. Successful
|
||||
changes apply to every app and widget that uses the integration.
|
||||
|
||||
When app creation is not permitted, the setup form offers linking an existing accessible app instead of attempting to
|
||||
create one.
|
||||
|
||||
When the connection unlocks useful widgets that are not on the current board yet, the confirmation also shows up to
|
||||
three explainable recommendations. A recommendation is based only on the services you can use and the widget kinds
|
||||
already present on that board. You can open the existing widget picker prefiltered to the recommendation, or dismiss
|
||||
recommendations for that service on that board.
|
||||
|
||||
## Managing permissions
|
||||
|
||||
On the edit page of an integration, you can manage which users / groups can
|
||||
|
||||
@@ -8,10 +8,19 @@ tags:
|
||||
|
||||
# Model Context Protocol (MCP)
|
||||
|
||||
Homarr exposes a [Model Context Protocol](https://modelcontextprotocol.io) server at `/api/mcp/mcp`, enabling AI assistants like Claude, Cursor, VS Code Copilot, and Claude Code to interact with your Homarr instance using natural language.
|
||||
Homarr exposes a [Model Context Protocol](https://modelcontextprotocol.io) server at `/api/mcp`, enabling AI assistants like Claude, Cursor, VS Code Copilot, and Claude Code to interact with your Homarr instance using natural language.
|
||||
|
||||
The endpoint implements the stateless MCP 2026-07-28 protocol and accepts 2025 Streamable HTTP clients through a
|
||||
stateless compatibility path. It does not create protocol sessions or require sticky sessions. Homarr creates the
|
||||
server and extracts its tools only after an authenticated request reaches the route. The previous `/api/mcp/mcp` URL
|
||||
remains available as a compatibility alias, but new clients should always use `/api/mcp`.
|
||||
|
||||
With MCP, you can ask your AI assistant to manage apps, integrations, users, boards, Docker containers, media servers, DNS settings, and more - all through Homarr's existing tRPC API, now exposed as 50+ AI-callable tools.
|
||||
|
||||
The optional [Homarr Assistant](./assistant) uses these same tool definitions internally.
|
||||
Unlike external MCP clients, it uses the current Homarr session directly and asks for in-conversation
|
||||
approval before running mutating procedures.
|
||||
|
||||
## Getting Started
|
||||
|
||||
The MCP endpoint lives under the **API** page at `Management` → `Tools` → `API` → **MCP** tab.
|
||||
@@ -42,7 +51,7 @@ Add the following to your client's MCP configuration file:
|
||||
{
|
||||
"mcpServers": {
|
||||
"homarr": {
|
||||
"url": "https://your-homarr-instance.com/api/mcp/mcp",
|
||||
"url": "https://your-homarr-instance.com/api/mcp",
|
||||
"headers": {
|
||||
"ApiKey": "<your-api-key>"
|
||||
}
|
||||
@@ -73,7 +82,7 @@ Replace `<your-api-key>` with your actual API key. Never commit API keys to vers
|
||||
"mcpServers": {
|
||||
"homarr": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "mcp-remote", "https://your-homarr-instance.com/api/mcp/mcp", "--header", "ApiKey:<your-api-key>"]
|
||||
"args": ["-y", "mcp-remote", "https://your-homarr-instance.com/api/mcp", "--header", "ApiKey:<your-api-key>"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,6 +105,10 @@ Custom Widget and Workshop authoring are different: their tools, prompts, and re
|
||||
key owner is a Homarr administrator. Non-administrator keys cannot discover or invoke those capabilities. Creating an
|
||||
API key never elevates the owner.
|
||||
|
||||
OAuth authorization and token requests bind issued credentials to the canonical `/api/mcp` resource. Dynamic client
|
||||
registration remains available for compatibility with current MCP clients while the ecosystem transitions toward
|
||||
pre-registered clients and Client ID Metadata Documents.
|
||||
|
||||
## Testing the Connection
|
||||
|
||||
Verify your MCP endpoint is working:
|
||||
@@ -103,13 +116,17 @@ Verify your MCP endpoint is working:
|
||||
```bash
|
||||
curl -s -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json, text/event-stream" \
|
||||
-H "Accept: application/json" \
|
||||
-H "ApiKey: <your-api-key>" \
|
||||
-d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}' \
|
||||
https://your-homarr-instance.com/api/mcp/mcp
|
||||
-H "Mcp-Method: server/discover" \
|
||||
-H "Mcp-Name: homarr" \
|
||||
-d '{"jsonrpc":"2.0","method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"test","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{}}},"id":1}' \
|
||||
https://your-homarr-instance.com/api/mcp
|
||||
```
|
||||
|
||||
A successful response includes the server capabilities and available tools. Use `tools/list` to see all available tools:
|
||||
A successful response includes the supported protocol versions and server capabilities. Use `tools/list` to see all
|
||||
available tools. MCP v2 clients add the per-request metadata and `Mcp-Method` / `Mcp-Name` headers automatically; the
|
||||
abbreviated request below uses the stateless 2025 compatibility path for readability:
|
||||
|
||||
```bash
|
||||
curl -s -X POST \
|
||||
@@ -117,33 +134,36 @@ curl -s -X POST \
|
||||
-H "Accept: application/json, text/event-stream" \
|
||||
-H "ApiKey: <your-api-key>" \
|
||||
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}' \
|
||||
https://your-homarr-instance.com/api/mcp/mcp
|
||||
https://your-homarr-instance.com/api/mcp
|
||||
```
|
||||
|
||||
Tool catalogs are returned in deterministic order and may be cached privately by the client for five minutes. The
|
||||
removed HTTP+SSE `/sse` and `/message` transports return `410 Gone`; use the single Streamable HTTP endpoint instead.
|
||||
|
||||
## Available Tools
|
||||
|
||||
Homarr exposes 50+ tools organized by namespace. Each tool is either a **query** (GET, read-only) or **mutation** (POST, write action).
|
||||
|
||||
| Namespace | Example Tools | Type |
|
||||
| ------------------ | --------------------------------------------------------- | ---------- |
|
||||
| `app` | List all apps, get app by ID, create/update/delete app | GET + POST |
|
||||
| `apiKeys` | List API keys | GET |
|
||||
| `beszel` | Systems status, alerts, historical system metrics | GET |
|
||||
| `board` | List boards, get board by ID, create/update/delete board | GET + POST |
|
||||
| `docker` | List containers, inspect container, start/stop/restart | GET + POST |
|
||||
| `icon` | Search icons | GET |
|
||||
| `info` | Server health, version info | GET |
|
||||
| `integration` | List integrations, test connection, delete integration | GET + POST |
|
||||
| `invite` | List invitations, delete invite | GET + POST |
|
||||
| `calendar` | Calendar events from media servers | GET |
|
||||
| `customWidget` | Validate, preview, create, and iteratively update widgets | GET + POST |
|
||||
| `dnsHole` | DNS hole summary, toggle blocking | GET + POST |
|
||||
| `downloads` | Active downloads, pause/resume/delete | GET + POST |
|
||||
| `healthMonitoring` | Resource usage stats | GET |
|
||||
| `patchmon` | Host patch and security update statistics | GET |
|
||||
| `mediaRequests` | Approve/decline media requests | GET + POST |
|
||||
| `mediaServer` | Media server sessions and stats | GET |
|
||||
| `smartHome` | Execute automations, device states | GET + POST |
|
||||
| Namespace | Example Tools | Type |
|
||||
| ------------------ | ---------------------------------------------------------- | ---------- |
|
||||
| `app` | List all apps, get app by ID, create/update/delete app | GET + POST |
|
||||
| `apiKeys` | List API keys | GET |
|
||||
| `beszel` | Systems status, alerts, historical system metrics | GET |
|
||||
| `board` | List boards, read settings/CSS, create/update/delete board | GET + POST |
|
||||
| `docker` | List containers, inspect container, start/stop/restart | GET + POST |
|
||||
| `icon` | Search icons | GET |
|
||||
| `info` | Server health, version info | GET |
|
||||
| `integration` | List integrations, test connection, delete integration | GET + POST |
|
||||
| `invite` | List invitations, delete invite | GET + POST |
|
||||
| `calendar` | Calendar events from media servers | GET |
|
||||
| `customWidget` | Validate, preview, create, and iteratively update widgets | GET + POST |
|
||||
| `dnsHole` | DNS hole summary, toggle blocking | GET + POST |
|
||||
| `downloads` | Active downloads, pause/resume/delete | GET + POST |
|
||||
| `healthMonitoring` | Resource usage stats | GET |
|
||||
| `patchmon` | Host patch and security update statistics | GET |
|
||||
| `mediaRequests` | Approve/decline media requests | GET + POST |
|
||||
| `mediaServer` | Media server sessions and stats | GET |
|
||||
| `smartHome` | Execute automations, device states | GET + POST |
|
||||
|
||||
The tools table on the MCP tab is dynamically generated - it always reflects the latest available procedures from your Homarr instance.
|
||||
|
||||
@@ -175,21 +195,36 @@ An agent creates one `homarr-custom-widget-v2` definition at a time. Start with
|
||||
The API key owner must be a Homarr administrator. The same rule applies to management, import, migration, preview,
|
||||
secrets, publication, Workshop installation, and the HTTP authoring resources.
|
||||
|
||||
Then, read the live schema and component resources. These resources describe the installed Mantine components.
|
||||
Load the installed skill before authoring. `customWidget_getSkill` returns the complete `SKILL.md` entrypoint and every
|
||||
bundled reference in one response, so an MCP client does not need filesystem access or a second documentation fetch.
|
||||
Then read the live schema and only the component resources required by the design. These resources describe the
|
||||
installed Mantine components.
|
||||
|
||||
1. Call `customWidget_getAuthoringPrompt` and `customWidget_schema`.
|
||||
2. Build one Custom Widget definition.
|
||||
3. Call `customWidget_validate` without saving the definition.
|
||||
4. Call `customWidget_previewCreate`.
|
||||
5. Test named requests with `customWidget_previewQuery` and `customWidget_previewAction`.
|
||||
6. Open the returned preview URL.
|
||||
7. Examine `customWidget_previewJournal`.
|
||||
8. Revise the local candidate and create another preview session until it is ready.
|
||||
9. Call `customWidget_create`, supplying any credentials and deployment-specific source URLs the user already provided.
|
||||
10. If the user must enter the source URL or credentials, call `customWidget_configurationRequestUser` with the new definition ID and wait for completion.
|
||||
11. Create a final preview with that definition ID so Homarr loads its stored credentials, then test it again.
|
||||
12. Use `customWidget_update` or `customWidget_templatePatch` for any final saved-definition corrections.
|
||||
13. Repeat this procedure for the next Custom Widget.
|
||||
1. Call `customWidget_getSkill`, `customWidget_getAuthoringPrompt`, `customWidget_schema`, and
|
||||
`customWidget_getComponentCatalog`.
|
||||
2. Fetch only the relevant installed component documentation with `customWidget_getComponent` and, when useful,
|
||||
one interaction example with `customWidget_getExample`. Component results contain component-specific props; collect
|
||||
any needed shared prop names from the compact catalog and fetch them together once with `customWidget_getSharedProps`.
|
||||
3. Build one Custom Widget definition. Use `templateLines` for generated multiline JSX to avoid a fragile JSON-escaped
|
||||
template string.
|
||||
4. Call `customWidget_validate` without saving the definition and repair every returned issue path.
|
||||
5. Call `customWidget_previewCreate`.
|
||||
6. Call `customWidget_previewQuery` for every entry in the returned `queries` list. Supply the listed invocation
|
||||
parameters for manual queries and inspect the real HTTP status and response shape before binding template fields.
|
||||
7. Test relevant actions with `customWidget_previewAction` in simulation mode.
|
||||
8. Open the returned `previewPath` or `previewUrl`.
|
||||
9. Examine `customWidget_previewJournal`.
|
||||
10. Revise the local candidate and create another preview session until validation, query data, actions, and rendering
|
||||
are ready.
|
||||
11. Call `customWidget_create`, supplying any credentials and deployment-specific source URLs the user already provided.
|
||||
12. Open the returned `managementPath` to review the exact saved definition.
|
||||
13. If the user must enter the source URL or credentials, call `customWidget_configurationRequestUser` with the new definition ID and wait for completion.
|
||||
14. Create a final preview with that definition ID so Homarr loads its stored credentials, then test it again.
|
||||
15. Use `customWidget_update` or `customWidget_templatePatch` for any final saved-definition corrections.
|
||||
16. Repeat this procedure for the next Custom Widget.
|
||||
|
||||
Validation and preview creation return compact lifecycle metadata. They do not echo the submitted widget or duplicate
|
||||
its template as both a string and line array; the agent should retain and reuse its local candidate.
|
||||
|
||||
The agent receives the configuration-request status. The agent does not receive plaintext credentials. A preview-scoped setup URL configures only that temporary preview; use a definition-scoped URL when the values must persist.
|
||||
|
||||
@@ -225,6 +260,10 @@ The Custom Widget tools are:
|
||||
- `customWidget_workshopGet`
|
||||
- `customWidget_workshopInstall`
|
||||
- `customWidget_getAuthoringPrompt`
|
||||
- `customWidget_getComponentCatalog`
|
||||
- `customWidget_getComponent`
|
||||
- `customWidget_getSharedProps`
|
||||
- `customWidget_getExample`
|
||||
- `customWidget_getSkill`
|
||||
|
||||
Custom Widget permissions are not assignable group capabilities. Administrator status is required for the complete
|
||||
|
||||
@@ -8,8 +8,7 @@ tags:
|
||||
# Search Engines
|
||||
|
||||
Using the search engines, terms can be quickly searched simply by opening the search bar at the top or using the keyboard shortcut `CTRL + K`.
|
||||
|
||||

|
||||
The management page provides a searchable list with the shortcut, URL template, source, and available row actions for each engine.
|
||||
|
||||
You can add any search engine that has a search URL that accepts a search term as a parameter.
|
||||
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
---
|
||||
tags:
|
||||
- Workshop
|
||||
- Custom Widgets
|
||||
- Custom CSS
|
||||
---
|
||||
|
||||
# Community Workshop
|
||||
|
||||
:::warning Experimental feature
|
||||
|
||||
This feature is experimental and will probably change. If you encounter bugs, need more capabilities, or find something confusing, tell us in the [official discussion thread](https://github.com/homarr-labs/homarr/discussions/categories/custom-widgets). Your feedback directly shapes what comes next. Happy hacking!
|
||||
|
||||
:::
|
||||
|
||||
The [Community Workshop](pathname:///workshop) is where Homarr users share and discover
|
||||
**custom CSS themes** and **custom widgets**. You can browse submissions, upvote the ones
|
||||
you like, report abuse, and install entries directly into your own Homarr instance.
|
||||
|
||||
The central `homarr.dev` Workshop is a public community. Browsing it on the website is public. Browsing, installing,
|
||||
publishing, or importing Workshop content **inside a Homarr instance is restricted to Homarr administrators**.
|
||||
|
||||
:::warning
|
||||
|
||||
The Workshop contains community-made content. Moderation is not a security boundary, and PocketBase does not interpret
|
||||
or validate submission content. Always review widget capabilities, network origins, actions, and CSS before installing.
|
||||
Homarr's installation-time validator is definitive and can reject content that the public service accepted earlier.
|
||||
Back up your instance before installing untrusted content.
|
||||
|
||||
:::
|
||||
|
||||
## Browsing and voting
|
||||
|
||||
- Filter by **Widgets** or **CSS**, show or hide outdated entries, and sort by popularity, creation date, recent updates, or discussion activity.
|
||||
- Sign in with **GitHub** to upvote/downvote, submit your own creations, or report a submission.
|
||||
- Each submission shows its title, description, linked GitHub author, revision, optional screenshots, comments, and full content. Widget pages also summarize API sources, actions, options, network access, and required credentials before installation.
|
||||
- A new submission receives one real upvote owned by its author. Votes remain unique per user and submission.
|
||||
- Report details and reporter identities are visible only to Workshop moderators. Reported submissions are highlighted in the moderation page, where each open report can be dismissed or the submission can be deleted. Reports do not automatically remove a submission.
|
||||
- The manual **Outdated** badge warns visitors that a submission may need an update.
|
||||
|
||||
## Comments
|
||||
|
||||
Comments are publicly readable and appear in chronological conversation bubbles with the author's avatar and GitHub profile. Signed-in users can post; authors can edit or delete their own comments, and Workshop moderators can delete any comment. Failed posts keep the text you typed. When SMTP and `WORKSHOP_PUBLIC_ORIGIN` are configured, the submission author receives one email for a comment from another user. Self-comments, edits, and deletions do not send mail.
|
||||
|
||||
## Submitting
|
||||
|
||||
1. Sign in with GitHub.
|
||||
2. Click **Share yours** and choose the type:
|
||||
- **Custom widget** — paste the JSON exported from **Manage → Custom Widgets → Export**.
|
||||
It must match the `homarr-custom-widget-v2` schema (validated before submission).
|
||||
- **Custom CSS** — paste your stylesheet (max 16384 characters).
|
||||
3. Review the widget JSON or CSS in the syntax-aware editor, then add a title, description, optional screenshots, and changelog.
|
||||
4. Submit the entry.
|
||||
|
||||
Submissions are published immediately. Reported entries are reviewed and may be removed.
|
||||
Authors can edit a submission later to publish a new revision. The edit form can keep or remove existing screenshots and add new PNG, JPG, or WebP images without changing the Workshop URL.
|
||||
|
||||
When SMTP and `WORKSHOP_PUBLIC_ORIGIN` are configured, every current Workshop moderator receives an email when a report is submitted. If a moderator deletes the reported submission, its author receives a separate removal email. Author self-deletion does not send a moderation email.
|
||||
|
||||
:::info Separate roles
|
||||
|
||||
A Workshop moderator is a user with PocketBase's internal `isAdmin` field. This role moderates the central community
|
||||
and is unrelated to a Homarr instance administrator.
|
||||
|
||||
:::
|
||||
|
||||
## Submission guidelines
|
||||
|
||||
Submissions must be useful to Homarr users and safe to install. Do not submit content that is
|
||||
malicious, deceptive, illegal, hateful, sexually explicit, gore, gambling-focused, spam, or content
|
||||
that primarily promotes unrelated products or services.
|
||||
|
||||
## Installing into Homarr
|
||||
|
||||
You can install workshop entries without leaving Homarr:
|
||||
|
||||
- **Custom widgets (Homarr administrators only)** — go to **Manage → Custom Widgets**, or use **Install from Workshop** in the
|
||||
board widget picker. Pick an entry, review its capabilities, and enter any required local secrets before import.
|
||||
- **Custom CSS from Workshop (Homarr administrators only)** — open a board's **Settings → Custom CSS**, click **Install from Workshop**, and
|
||||
pick a theme to load it into the editor before saving.
|
||||
|
||||
Alternatively, open a submission in the Workshop and use **Download widget JSON** or **Copy CSS**. The submission page shows the matching manual installation steps before the source code.
|
||||
|
||||
Custom Widget imports detect required bearer, basic, header, or query credentials. You can enter them during import or explicitly defer them; secret values are never stored in Workshop content. Missing credentials remain visible on the local Custom Widget afterward.
|
||||
|
||||
## Workshop deployment
|
||||
|
||||
Workshop is powered by [PocketBase](https://pocketbase.io). The production image compiles the current Docusaurus site and serves it together with the API from one origin. The deployment lives in `apps/workshop/` with a Dockerfile, Compose configuration, version-controlled migrations, hooks, a smoke test, and ACT-local workflow instructions.
|
||||
|
||||
Run `pnpm docker:workshop` for PocketBase on port 8090, then run
|
||||
`WORKSHOP_API_URL=http://127.0.0.1:8090 pnpm dev:docs` for Docusaurus on port 3003. `pnpm docker:docs` builds the
|
||||
combined production layout. Normal start and stop commands retain data; `down --volumes` is an intentional,
|
||||
irreversible development reset.
|
||||
|
||||
GitHub needs only the central callback `https://<workshop-host>/api/oauth2-redirect`. Keep PocketBase origins unrestricted
|
||||
unless every self-hosted Homarr caller is explicitly listed. Configure both `GITHUB_CLIENT_ID` and
|
||||
`GITHUB_CLIENT_SECRET`; supplying only one is a startup error. Set `WORKSHOP_PUBLIC_ORIGIN` in production so generated
|
||||
links and email use the public host. Workshop uses the OAuth provider username as the public display name instead of
|
||||
the profile's full name. Configure SMTP for comment, report, and removal notices.
|
||||
|
||||
The public URL contract is:
|
||||
|
||||
| Variable | Purpose | Default |
|
||||
| ------------------------ | ------------------------------------------------- | ----------------------------- |
|
||||
| `HOMARR_WEBSITE_URL` | Documentation/site base used by Homarr links | `https://homarr.dev` |
|
||||
| `WORKSHOP_API_URL` | PocketBase API used by Homarr and standalone docs | `HOMARR_WEBSITE_URL` |
|
||||
| `WORKSHOP_WEB_URL` | Public Workshop page linked from Homarr | `HOMARR_WEBSITE_URL/workshop` |
|
||||
| `WORKSHOP_PUBLIC_ORIGIN` | PocketBase-generated links and email | Required in production |
|
||||
| `PB_ALLOWED_ORIGINS` | PocketBase browser origins | `*` for the central community |
|
||||
|
||||
The deprecated docs-only `WORKSHOP_URL` alias remains for one release; `WORKSHOP_API_URL` takes precedence. The
|
||||
combined Workshop image writes these public URLs into its browser runtime configuration whenever the container starts;
|
||||
the Docker build arguments remain fallbacks. Restart the container after changing these values, without rebuilding it.
|
||||
|
||||
### Staging and demo deployment
|
||||
|
||||
Use `apps/workshop/docker-compose.staging.yml` with the published `ghcr.io/homarr-labs/workshop:v2` image for a v2
|
||||
preview. Copy `apps/workshop/.env.staging.example` to `.env.staging`; its default public contract is:
|
||||
|
||||
```dotenv
|
||||
HOMARR_WEBSITE_URL=https://v2.preview.homarr.dev
|
||||
WORKSHOP_API_URL=https://v2.preview.homarr.dev
|
||||
WORKSHOP_WEB_URL=https://v2.preview.homarr.dev/workshop
|
||||
WORKSHOP_PUBLIC_ORIGIN=https://v2.preview.homarr.dev
|
||||
PB_ALLOWED_ORIGINS=*
|
||||
```
|
||||
|
||||
Start or update it from the repository root:
|
||||
|
||||
```sh
|
||||
docker compose --env-file apps/workshop/.env.staging \
|
||||
-f apps/workshop/docker-compose.staging.yml pull
|
||||
docker compose --env-file apps/workshop/.env.staging \
|
||||
-f apps/workshop/docker-compose.staging.yml up --detach
|
||||
curl --fail http://127.0.0.1:8090/api/health
|
||||
```
|
||||
|
||||
The service binds to `127.0.0.1:8090`. Terminate TLS at a reverse proxy and forward the **entire hostname**, including
|
||||
`/api`, `/_/`, and `/workshop-runtime-config.js`; forwarding only `/workshop` breaks PocketBase and runtime URL loading.
|
||||
This combined image serves both the documentation site and PocketBase, so use a dedicated Workshop hostname if the
|
||||
chosen hostname already serves Homarr itself. In that case, replace the hostname in all four URL values and keep
|
||||
`/workshop` only on `WORKSHOP_WEB_URL`.
|
||||
|
||||
The Compose volume preserves PocketBase data across pulls and container recreation. The mutable `v2` tag is suitable
|
||||
for previews; use the `sha-<commit>` tag for promotion. For the public central Workshop, `PB_ALLOWED_ORIGINS=*` allows
|
||||
self-hosted Homarr origins to use the API and OAuth. A private demo may use a comma-separated origin allowlist instead.
|
||||
GitHub OAuth requires both credentials and the callback
|
||||
`https://v2.preview.homarr.dev/api/oauth2-redirect`. Restart after changing environment values, then verify the health
|
||||
endpoint, `/workshop`, a public listing request, GitHub sign-in, and an installation from the Homarr preview.
|
||||
|
||||
Back up application and PocketBase data before deployment. Publish the combined image by immutable digest, test the
|
||||
exact digest in staging, rehearse application rollback and PocketBase restore, then promote that same digest.
|
||||
|
||||
## Client architecture
|
||||
|
||||
Workshop access is centralized in `@homarr/workshop/backend`. It exports the typed `WorkshopBackend`, its typed PocketBase instance, stable TanStack Query option factories, and the `useWorkshop…` query and mutation hooks used by Homarr. The documentation site uses the same backend rather than maintaining a second API contract.
|
||||
|
||||
Listing fields derived by the `workshop_listings` view—such as submission type, score, and counts—are normalized by the shared client. Healthy deployments keep filtering, sorting, and pagination in PocketBase. If a persistent development volume still has an older view that rejects those fields, the client retries without them and applies the same operations locally; it can also fall back to the base submissions collection. Production deployments should nevertheless apply the current migration so the view exposes the complete metadata.
|
||||
@@ -17,6 +17,8 @@ import { anchorIntegration } from "@site/docs/integrations/anchor";
|
||||
|
||||
The Anchor Note widget shows a selected note from your Anchor integration. If your note permissions allow it, you can also edit the note title and content directly in Homarr.
|
||||
|
||||
Advanced view always shows the note title and update time alongside the complete formatted document, permission, creation date, and pinned or archived state. Compact title and update-time switches do not hide that information in advanced view.
|
||||
|
||||
### Screenshots
|
||||
|
||||
import anchorNoteWidgetScreenshot from "./img/widget.png";
|
||||
|
||||
@@ -14,7 +14,7 @@ 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 keeps the current project, runner state, job counts, bandwidth, and optional broadcast message visible. Advanced view adds the complete active-job list, job start times, project and bandwidth-session identifiers, session transfer totals, exact update time, and the full broadcast message whenever one is available. A failed refresh keeps the last snapshot visible with a stale-data warning.
|
||||
|
||||
### Supported integration
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
title: "Assistant"
|
||||
description: "Have a complete assistant conversation directly on a board."
|
||||
hide_title: true
|
||||
---
|
||||
|
||||
import { AddingWidget } from "@site/src/components/widgets/adding";
|
||||
import { WidgetConfig } from "@site/src/components/widgets/configuration";
|
||||
import { WidgetHeader } from "@site/src/components/widgets/header";
|
||||
import { assistantWidget } from ".";
|
||||
|
||||
<WidgetHeader widget={assistantWidget} categories={["Productivity"]} />
|
||||
|
||||
The Assistant widget brings the same conversation available from the user menu and command menu onto a
|
||||
board. Messages, conversations, tool calls, approvals, files, mentions, model controls, reasoning controls,
|
||||
usage details, and response actions remain synchronized with the app-wide assistant.
|
||||
|
||||
The assistant must be enabled by an administrator in **Management → Assistant**. Signed-out users and users
|
||||
on an instance without an enabled model see an unavailable state instead of a broken composer.
|
||||
|
||||
### Adding the widget
|
||||
|
||||
<AddingWidget />
|
||||
|
||||
The default widget size is ten columns by four rows and automatically fits boards with fewer columns. Controls
|
||||
and labels condense as the widget becomes narrower, while the conversation remains scrollable and usable on
|
||||
touch devices. When a board layout does not provide enough height for conversation controls, the widget shows
|
||||
a compact **Open assistant** action instead of squeezing an unusable composer into the card. Increase its height
|
||||
in that layout to keep the complete conversation inline.
|
||||
|
||||
### Configuration
|
||||
|
||||
<WidgetConfig configuration={assistantWidget.configuration} />
|
||||
|
||||
**Current conversation** follows whichever conversation is active in the user-menu panel, `Shift + A`, or
|
||||
the command menu. This is the default and keeps every assistant entry point synchronized.
|
||||
|
||||
**Pinned conversation** reveals a searchable conversation selector. A pinned widget never changes the
|
||||
active conversation silently: select **Open conversation** inside the widget before continuing that thread.
|
||||
|
||||
### Conversation features
|
||||
|
||||
- Create, switch, rename, and delete conversations.
|
||||
- Send text and supported file or image attachments by picker, drag and drop, or image paste.
|
||||
- Mention permitted apps, integrations, boards, widgets, and tools with `@`.
|
||||
- Use `/` shortcuts for common dashboard tasks.
|
||||
- Review tool calls, approve mutations, and answer structured assistant questions inline.
|
||||
- Answer pending structured questions from the dock above the composer without scrolling through the conversation.
|
||||
- Enable conversation-scoped automatic approvals from the shield control to approve protected mutations, use presented form details, and choose answers categorized as affirmative without individual clicks.
|
||||
- Edit user messages and copy, regenerate, branch, quote, export, or rate responses.
|
||||
- Inspect syntax-highlighted Markdown, sources, generated files, tool results, streaming chain-of-thought,
|
||||
cumulative conversation usage, per-turn token and context use, timing, speed, provider routing, and cost.
|
||||
- Change the model and reasoning effort for the active conversation.
|
||||
- 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.
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { WidgetDefinition } from "@site/src/types";
|
||||
import { IconRobot } from "@tabler/icons-react";
|
||||
|
||||
export const assistantWidget: WidgetDefinition = {
|
||||
icon: IconRobot,
|
||||
name: "Assistant",
|
||||
description: "Have a complete assistant conversation directly on a board.",
|
||||
path: "../../widgets/assistant",
|
||||
configuration: {
|
||||
items: [
|
||||
{
|
||||
name: "Conversation",
|
||||
description: "Follow the active conversation or attach the widget to one persisted conversation.",
|
||||
values: { type: "select", options: ["Current conversation", "Pinned conversation"] },
|
||||
defaultValue: "Current conversation",
|
||||
},
|
||||
{
|
||||
name: "Pinned conversation",
|
||||
description: "Search for a conversation. This field is hidden when Current conversation is selected.",
|
||||
values: { type: "string" },
|
||||
defaultValue: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -36,7 +36,9 @@ import widgetOverview from "./img/widget-overview.png";
|
||||
]}
|
||||
/>
|
||||
|
||||
<WidgetInteractionGuide advancedView={false} />
|
||||
Advanced view shows every missing-subtitle and health summary regardless of compact visibility options, together with the already-fetched Sonarr and Radarr SignalR states and announcement count.
|
||||
|
||||
<WidgetInteractionGuide advancedView />
|
||||
|
||||
### Adding the widget
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ This widget displays your Beszel alert configurations separated by status (trigg
|
||||
]}
|
||||
/>
|
||||
|
||||
<WidgetInteractionGuide />
|
||||
<WidgetInteractionGuide advancedView={false} />
|
||||
|
||||
### Adding the widget
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ description: "Card grid view of all Beszel-monitored systems with real-time metr
|
||||
hide_title: true
|
||||
---
|
||||
|
||||
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
|
||||
|
||||
import { WidgetHeader } from "@site/src/components/widgets/header";
|
||||
import { AddingWidget } from "@site/src/components/widgets/adding";
|
||||
import { WidgetConfig } from "@site/src/components/widgets/configuration";
|
||||
@@ -34,6 +36,8 @@ Select a system card to open the complete statistics view, including system, fil
|
||||
]}
|
||||
/>
|
||||
|
||||
<WidgetInteractionGuide advancedView={false} />
|
||||
|
||||
### Adding the widget
|
||||
|
||||
<AddingWidget />
|
||||
|
||||
@@ -4,6 +4,8 @@ description: "Table view of all Beszel-monitored systems with sortable columns a
|
||||
hide_title: true
|
||||
---
|
||||
|
||||
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
|
||||
|
||||
import { WidgetHeader } from "@site/src/components/widgets/header";
|
||||
import { AddingWidget } from "@site/src/components/widgets/adding";
|
||||
import { WidgetConfig } from "@site/src/components/widgets/configuration";
|
||||
@@ -35,6 +37,8 @@ Select a system row to open the complete statistics view.
|
||||
]}
|
||||
/>
|
||||
|
||||
<WidgetInteractionGuide advancedView={false} />
|
||||
|
||||
### Adding the widget
|
||||
|
||||
<AddingWidget />
|
||||
|
||||
@@ -16,6 +16,7 @@ import { bookmarksWidget } from ".";
|
||||
The bookmarks widget allows you to display multiple app links in a single widget.
|
||||
You can add links to your favorite applications, websites, or any other resources you want quick access to.
|
||||
Its normal view adapts each configured layout to the available width and height: lower-priority hostnames are hidden before names or icons, and larger collections scroll instead of crushing cards.
|
||||
Advanced view shows every bookmark as a roomy card with its full title, hostname, icon, and complete description regardless of the compact layout and visibility choices.
|
||||
|
||||
### Screenshots
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ import { icalIntegration } from "@site/docs/integrations/ical";
|
||||
|
||||
The calendar widget shows a month view of events from different integrations.
|
||||
It allows you to view calendar events or upcoming releases from your media managers.
|
||||
Calendar requests use normal human month numbers (`1` for January through `12` for December), including when
|
||||
the same data is requested through Homarr's MCP or Assistant tools.
|
||||
|
||||
### Screenshots
|
||||
|
||||
@@ -72,7 +74,7 @@ import withoutIntegrations from "./img/without-integrations.png";
|
||||
### Interactions
|
||||
|
||||
- Hover over a marked date to preview its events in a popover.
|
||||
- The advanced view replaces the month grid with a grouped event list. Use its previous and next controls to change months.
|
||||
- The advanced view replaces the month grid with a grouped event list, keeps every event attributed to its source integration, and expands titles, locations, and descriptions. Use its previous and next controls to change months.
|
||||
- If one selected integration is unavailable, its warning indicator remains visible while events from healthy integrations continue to load.
|
||||
|
||||
<WidgetInteractionGuide />
|
||||
|
||||
@@ -27,7 +27,7 @@ import tokyoClock from "./img/tokyo-12-hour.png";
|
||||
<img src={tokyoClock} alt="clock format-12-hours timezone-tokyo" width={256} height={128} />
|
||||
</div>
|
||||
|
||||
<WidgetInteractionGuide />
|
||||
<WidgetInteractionGuide advancedView={false} />
|
||||
|
||||
### Adding the widget
|
||||
|
||||
|
||||
@@ -27,7 +27,11 @@ The Coolify widget displays a compact overview of your Coolify instance. It can
|
||||
]}
|
||||
/>
|
||||
|
||||
<WidgetInteractionGuide advancedView={false} />
|
||||
Advanced view opens the Servers, Applications, and Services sections regardless of their compact visibility switches. It identifies each Coolify integration, shows server `is_reachable` and `is_usable` values, and lists applications nested in services when the API provides them. Healthy integrations remain visible if another source fails, with the failed source identified by a warning.
|
||||
|
||||
IP addresses remain masked in advanced view until you explicitly use the eye control. Public resource, Coolify dashboard, and log links retain the widget's safe new-tab handling.
|
||||
|
||||
<WidgetInteractionGuide advancedView />
|
||||
|
||||
### Adding the widget
|
||||
|
||||
|
||||
@@ -39,6 +39,9 @@ Action buttons can only be executed by administrators.
|
||||
|
||||
Each named request declares its minimum board permission. This permission does not depend on the HTTP method.
|
||||
|
||||
Anonymous visitors can load queries from a Custom API widget on a public board. Definitions placed only on private
|
||||
boards remain unavailable to them.
|
||||
|
||||
Queries and actions can use GET, POST, PUT, or PATCH. DELETE is valid only for actions so it can never run
|
||||
automatically while a board loads. The `kind` value controls the remaining runtime behavior.
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ import { technitiumDnsIntegration } from "@site/docs/integrations/technitium-dns
|
||||
|
||||
<WidgetHeader widget={dnsHoleControlsWidget} categories={["DNS Hole"]} />
|
||||
|
||||
When several DNS integrations are selected, Homarr keeps healthy controls available if one source fails and shows a warning identifying the unavailable integration.
|
||||
|
||||
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's width and height.
|
||||
|
||||
### Screenshots
|
||||
|
||||
@@ -17,6 +17,8 @@ import { dnsHoleSummaryWidget } from ".";
|
||||
|
||||
<WidgetHeader widget={dnsHoleSummaryWidget} categories={["DNS Hole"]} />
|
||||
|
||||
When several DNS integrations are selected, Homarr keeps healthy sources visible if one source fails and shows a warning identifying the unavailable integration.
|
||||
|
||||
This widget combines statistics from every selected DNS service, including total queries, blocked queries, and blocklist size. Its data is cached under the same DNS query family as DNS Hole Controls, so placing both widgets on a board does not create a second integration request. When the widget has enough room, it adds a scrollable status row naming each selected source.
|
||||
|
||||
### Screenshots
|
||||
|
||||
@@ -45,7 +45,7 @@ import contextMenuImg from "./img/context-menu.png";
|
||||
- **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
|
||||
- **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
|
||||
- **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
|
||||
- **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
|
||||
|
||||
@@ -23,6 +23,8 @@ import { glancesIntegration } from "@site/docs/integrations/glances";
|
||||
|
||||
This widget displays information about the health and status of your system(s), including CPU, memory, disk usage, and more.
|
||||
|
||||
Advanced view is a complete health workspace: it shows every available CPU, temperature, memory, GPU, filesystem, update, reboot, and system-information section even when compact visibility switches hide some of them on the board. Proxmox clusters likewise show uptime, CPU, memory, and every node, VM, container, and storage section. Storage-volume filters, units, and permissions remain in effect.
|
||||
|
||||
### Screenshots
|
||||
|
||||
import systemView from "./img/system-view.png";
|
||||
|
||||
@@ -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 groups previous, pause or resume, and next together and adds a thumbnail strip for quick navigation.
|
||||
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. If an Immich refresh fails, the last loaded album stays visible with a stale-data warning.
|
||||
|
||||
### Adding the widget
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ import { sonarrIntegration } from "@site/docs/integrations/sonarr";
|
||||
|
||||
Monitor which movies and episodes are missing from your library, and track what is currently being downloaded. The widget pulls data from Radarr and Sonarr and groups results by type in two tabs.
|
||||
|
||||
Advanced view always shows both Missing and Queued panels side by side when space allows, requests at least 50 items per source, exposes queue status and time remaining, and keeps partial-integration warnings visible. Compact tab visibility and page-size choices continue to shape the board tile.
|
||||
|
||||
### Screenshots
|
||||
|
||||
import widgetMissing from "./img/missing.png";
|
||||
|
||||
@@ -19,6 +19,10 @@ import { plexIntegration } from "@site/docs/integrations/plex";
|
||||
|
||||
Provides a list of newly added media or upcoming releases from your media server.
|
||||
|
||||
Releases retain the name and icon of the integration that supplied them. If one integration is unavailable, releases from healthy integrations remain visible and a warning identifies the failed source without exposing connection details. The widget reports an error only when every selected integration fails.
|
||||
|
||||
Advanced view displays every release in a roomy poster layout with its complete available metadata, description, tags, and visible source name, regardless of the compact layout and visibility options.
|
||||
|
||||
### Screenshots
|
||||
|
||||
import backdrop from "./img/backdrop.png";
|
||||
|
||||
@@ -24,7 +24,7 @@ import { plexIntegration } from "@site/docs/integrations/plex";
|
||||
The media server widget shows active sessions across your media servers.
|
||||
It shows playback progress, pause state, time remaining, and whether each session is direct play, direct stream, or transcoding audio or video. Location and bitrate can be toggled independently.
|
||||
|
||||
Select a session to inspect its media metadata. Advanced mode adds search, sorting, and a summary of playing and transcoding sessions while keeping separate integration instances identifiable.
|
||||
Select a session to inspect its media metadata in an anchored popover. Advanced mode adds search, sorting, location, bitrate, and a summary of playing and transcoding sessions while keeping separate integration instances identifiable. Compact visibility switches do not hide location or bitrate from the complete advanced session view.
|
||||
If one selected server is unavailable, a warning indicator identifies the degraded result while sessions from healthy servers remain usable.
|
||||
Sessions without user metadata remain visible and are labeled as an unknown user.
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import { tdarrIntegration } from "@site/docs/integrations/tdarr";
|
||||
|
||||
The media transcoding widget provides you with an overview of your media transcoding status.
|
||||
It shows you the current queue, worker status, and statistics about your transcoding jobs.
|
||||
Advanced view presents all three panels together. If refreshes fail after data has loaded, Homarr keeps the last useful result visible and marks it as stale.
|
||||
|
||||
### Screenshots
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ import { networkControllerStatusWidget } from ".";
|
||||
|
||||
Shows connected users and connected guest devices on wired or wireless networks.
|
||||
|
||||
Compact view aggregates the configured controllers into the selected Wi-Fi or wired total. Advanced view shows both totals separately for every controller, preserving each integration's name, type, and exact update time while keeping partial-source and stale-data warnings visible.
|
||||
|
||||
### Screenshots
|
||||
|
||||
import wifiCard from "./img/wifi.png";
|
||||
|
||||
@@ -36,7 +36,9 @@ import summaryCard from "./img/summary.png";
|
||||
]}
|
||||
/>
|
||||
|
||||
<WidgetInteractionGuide advancedView={false} />
|
||||
In advanced view, each controller gets its own information matrix showing source, last update, WAN and web status, latency, ping, web uptime, separate Wi-Fi and LAN user and guest counts, and connected VPN users. The compact board view keeps its progressive aggregate layout.
|
||||
|
||||
<WidgetInteractionGuide />
|
||||
|
||||
### Adding the widget
|
||||
|
||||
|
||||
@@ -34,7 +34,9 @@ import notificationsScreenshot from "./img/notifications.png";
|
||||
items={[{ integration: ntfyIntegration }, { integration: nextcloudIntegration }, { integration: gotifyIntegration }]}
|
||||
/>
|
||||
|
||||
<WidgetInteractionGuide advancedView={false} />
|
||||
Advanced view always shows available notification logos and sources and removes the compact body line limit. External notification links retain the same safe new-tab behavior.
|
||||
|
||||
<WidgetInteractionGuide advancedView />
|
||||
|
||||
### Adding the widget
|
||||
|
||||
|
||||
@@ -72,6 +72,10 @@ The widget adapts its layout based on the available space:
|
||||
|
||||
When the widget has enough space, a "last updated" timestamp appears at the bottom showing how recently PatchMon refreshed its data. Narrow layouts use a shortened timestamp such as `5m ago`; wider short layouts can show the full translated text.
|
||||
|
||||
### Advanced view
|
||||
|
||||
Advanced view is a complete PatchMon posture view. It always shows the compliance hero, all eight host and package statistics, the full OS distribution with versions, and the last-updated time. Compact visibility switches and OS limits still shape the board tile, but do not hide already-fetched security information in advanced view. Threshold and chart-style choices remain in effect.
|
||||
|
||||
### Supported Integrations
|
||||
|
||||
<WidgetIntegrations
|
||||
@@ -83,7 +87,7 @@ When the widget has enough space, a "last updated" timestamp appears at the bott
|
||||
]}
|
||||
/>
|
||||
|
||||
<WidgetInteractionGuide advancedView={false} />
|
||||
<WidgetInteractionGuide />
|
||||
|
||||
### Adding the widget
|
||||
|
||||
|
||||
@@ -35,7 +35,9 @@ GitHub, Docker Hub, GitLab, NPM, Codeberg, GitHub Container Registry, Quay, and
|
||||
For GitHub Container Registry, use `owner/package` identifiers such as `homarr-labs/homarr`.
|
||||
Full image references such as `ghcr.io/homarr-labs/homarr:latest` are also accepted.
|
||||
|
||||
<WidgetInteractionGuide advancedView={false} />
|
||||
Advanced view shows every configured repository even when compact view is limited to highlighted or top releases. Repository names and details remain visible, and selecting a repository still expands its description and links.
|
||||
|
||||
<WidgetInteractionGuide />
|
||||
|
||||
### Adding the widget
|
||||
|
||||
|
||||
@@ -21,7 +21,9 @@ import screenshot from "./img/screenshot.png";
|
||||
|
||||
<img src={screenshot} alt="rss-feed noted-lol" width={128 * 4} />
|
||||
|
||||
<WidgetInteractionGuide advancedView={false} />
|
||||
Advanced view keeps the configured feeds and post limit unchanged while showing each fetched post's full description, image, source, relative time, and exact publication timestamp. Right-to-left text and safe external links continue to follow the widget configuration.
|
||||
|
||||
<WidgetInteractionGuide advancedView />
|
||||
|
||||
### Adding the widget
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ The entity state enables you to display the current state of an entity.
|
||||
This works with most entities and enables you display almost any data.
|
||||
It is recommended that you transform data on the integration as much as possible since Homarr's capabilities in transformation are very limited.
|
||||
|
||||
Advanced view shows the safe Home Assistant metadata owned by Homarr: the friendly name, entity ID, unit, device class, icon, last-changed time, and last-updated time. Arbitrary attributes remain intentionally excluded.
|
||||
|
||||
### Screenshots
|
||||
|
||||
import sun from "./img/sun.png";
|
||||
|
||||
@@ -20,6 +20,8 @@ import { homeAssistantIntegration } from "@site/docs/integrations/home-assistant
|
||||
|
||||
The execute automation widget allows you to trigger an automation with one click.
|
||||
|
||||
Advanced view shows the automation ID, interaction permission, last successful run in the current browser session, and the latest action error together. Session-only history is not presented as Home Assistant history.
|
||||
|
||||
### Screenshots
|
||||
|
||||
import empty from "./img/empty.png";
|
||||
|
||||
@@ -18,6 +18,10 @@ import { speedtestTrackerIntegration } from "@site/docs/integrations/speedtest-t
|
||||
This widget displays internet speed test results from your [Speedtest Tracker](https://docs.speedtest-tracker.dev/) instance,
|
||||
including the latest result, historical averages, and a live chart of the last 12 hours.
|
||||
|
||||
Advanced view renders a source-labeled panel for every healthy integration. Each panel exposes that source's latest test, statistics, speed and ping charts, plus 12-hour, 24-hour, and truthful **Last 30** result ranges regardless of compact visibility switches. Compact mode keeps the responsive aggregate overview and names its healthy sources when multiple integrations are selected.
|
||||
|
||||
When multiple Speedtest Tracker integrations are selected, results from healthy sources remain visible if another source fails. A warning indicator identifies each unavailable integration without exposing private request details. If every selected integration fails, the widget reports an error; if cached data remains visible after a later total failure, a stale-data warning is shown.
|
||||
|
||||
### Screenshots
|
||||
|
||||
import screenshot from "./img/screenshot.png";
|
||||
|
||||
@@ -16,6 +16,8 @@ import { stockPriceWidget } from ".";
|
||||
Using the Stocks widget, you can show the current stock prices of your favorite companies on your dashboard.
|
||||
The widget prioritizes the symbol, current price, and trend. Company name, change, time range, and a taller chart appear progressively when both widget dimensions can support them.
|
||||
|
||||
In advanced view, the widget always shows the returned symbol and company name, configured range and interval, current and previous close, absolute and percentage change, range minimum and maximum, and the full price-history chart. These values use the same configured Yahoo Finance query as the compact widget; advanced view does not change the symbol, range, or interval.
|
||||
|
||||
### Screenshots
|
||||
|
||||
import apple from "./img/apple.png";
|
||||
@@ -34,7 +36,7 @@ import nvidia from "./img/nvidia.png";
|
||||
<img src={nvidia} alt="stock-price nvidia" width={128 * 2 + 2} height={128 * 2 + 4} />
|
||||
</div>
|
||||
|
||||
<WidgetInteractionGuide advancedView={false} />
|
||||
<WidgetInteractionGuide />
|
||||
|
||||
### Adding the widget
|
||||
|
||||
|
||||
@@ -44,7 +44,9 @@ This widget displays filesystem and SMART disk usage for supported server and NA
|
||||
]}
|
||||
/>
|
||||
|
||||
<WidgetInteractionGuide advancedView={false} />
|
||||
Advanced view keeps your configured volume filters while showing each integration name, usage percentage, used and total space, free-space percentage, SMART status, available temperature, and the usage background bar regardless of compact display options.
|
||||
|
||||
<WidgetInteractionGuide />
|
||||
|
||||
### Adding the widget
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ import { tracearrIntegration } from "@site/docs/integrations/tracearr";
|
||||
|
||||
The Tracearr widget provides a real-time monitoring dashboard for your media server activity. It displays active streams, user statistics, recent watching history, and policy violations.
|
||||
|
||||
When multiple Tracearr integrations are selected, every stream, violation, and activity row keeps its source identity and an integration-scoped key. Data from healthy sources remains visible if another source fails, and a warning identifies each unavailable integration without exposing private request details. If every selected integration fails, the widget reports an error; if cached data remains visible after a later total failure, a stale-data warning is shown.
|
||||
|
||||
### Sections
|
||||
|
||||
| Section | Description |
|
||||
|
||||
@@ -17,6 +17,10 @@ import { traefikIntegration } from "@site/docs/integrations/traefik";
|
||||
|
||||
This widget displays a compact overview of your Traefik instance, including total routers, entry points, protocol-specific router/service/middleware counts, and warning or error totals when Traefik reports them.
|
||||
|
||||
Advanced view shows every protocol and entry point, identifies every healthy Traefik source and version, and labels each resource or failed endpoint with its owning integration. Compact mode keeps the responsive aggregate overview and names each healthy source when multiple integrations are selected.
|
||||
|
||||
When multiple Traefik integrations are selected, data from healthy sources remains visible if another source fails. A warning indicator identifies each unavailable integration without exposing private request details. If every selected integration fails, the widget reports an error; if cached data remains visible after a later total failure, a stale-data warning is shown.
|
||||
|
||||
### Supported Integrations
|
||||
|
||||
<WidgetIntegrations
|
||||
|
||||
@@ -19,6 +19,10 @@ This widget displays the status of the UPS devices reported by a Network UPS Too
|
||||
status (online, on battery, charging or low battery), battery charge and estimated runtime, output load, and input and
|
||||
output voltage.
|
||||
|
||||
Every device keeps an integration-scoped identity. Advanced view always labels each device with its owning integration, while compact layouts add the source label only when multiple healthy integrations are present.
|
||||
|
||||
When multiple UPS integrations are selected, devices from healthy sources remain visible if another source fails. A warning indicator identifies each unavailable integration without exposing private request details. If every selected integration fails, the widget reports an error; if cached data remains visible after a later total failure, a stale-data warning is shown.
|
||||
|
||||
### Layouts
|
||||
|
||||
import screenshotFull from "./img/screenshot.png";
|
||||
|
||||
@@ -17,6 +17,10 @@ import { uptimeKumaIntegration } from "@site/docs/integrations/uptime-kuma";
|
||||
|
||||
This widget displays uptime statistics from your Uptime Kuma status page, including a visual uptime ring, average uptime percentage, and per-status monitor counts.
|
||||
|
||||
Advanced view always shows the uptime ring, all status totals, and the complete monitor list with source identity, regardless of compact visibility switches.
|
||||
|
||||
When multiple Uptime Kuma integrations are selected, monitors from healthy sources remain visible if another source fails. A warning indicator identifies each unavailable integration without exposing private request details. If every selected integration fails, the widget reports an error; if cached data remains visible after a later total failure, a stale-data warning is shown.
|
||||
|
||||
### Supported Integrations
|
||||
|
||||
<WidgetIntegrations
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: "What's Up Docker"
|
||||
description: "Displays how many watched containers have image updates available."
|
||||
hide_title: true
|
||||
---
|
||||
|
||||
import { WidgetHeader } from "@site/src/components/widgets/header";
|
||||
import { AddingWidget } from "@site/src/components/widgets/adding";
|
||||
import { WidgetConfig } from "@site/src/components/widgets/configuration";
|
||||
import { WidgetIntegrations } from "@site/src/components/widgets/integrations";
|
||||
import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions";
|
||||
import { wudWidget } from ".";
|
||||
import { wudIntegration } from "@site/docs/integrations/whats-up-docker";
|
||||
|
||||
<WidgetHeader widget={wudWidget} categories={["Health Monitoring"]} />
|
||||
|
||||
This widget shows two stats from your WUD instance: the total number of monitored containers, and how many of them currently have an image update available. Optionally, it can also list each outdated container with its current and new version, linking to the release notes when WUD provides one.
|
||||
|
||||
### Supported Integrations
|
||||
|
||||
<WidgetIntegrations
|
||||
items={[
|
||||
{
|
||||
integration: wudIntegration,
|
||||
note: "Displays monitored container and available-update counts",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<WidgetInteractionGuide />
|
||||
|
||||
In advanced view, the widget always shows its title, update ring, monitored-container summary, and the full list of available container updates. The configured horizontal or vertical layout still applies. Visibility options only affect the compact board view. A failed refresh keeps the last WUD snapshot visible with a stale-data warning.
|
||||
|
||||
### Adding the widget
|
||||
|
||||
<AddingWidget />
|
||||
|
||||
### Configuration
|
||||
|
||||
<WidgetConfig configuration={wudWidget.configuration} />
|
||||
@@ -0,0 +1,38 @@
|
||||
import { WidgetDefinition } from "@site/src/types";
|
||||
import { IconBrandDocker } from "@tabler/icons-react";
|
||||
|
||||
export const wudWidget: WidgetDefinition = {
|
||||
icon: IconBrandDocker,
|
||||
name: "What's Up Docker",
|
||||
description: "Displays how many watched containers have image updates available.",
|
||||
path: "../../widgets/whats-up-docker",
|
||||
configuration: {
|
||||
items: [
|
||||
{
|
||||
name: "Show title",
|
||||
description: 'Displays the icon and "What\'s Up Docker" label above the stats',
|
||||
values: { type: "boolean" },
|
||||
defaultValue: "yes",
|
||||
},
|
||||
{
|
||||
name: "Show update ring",
|
||||
description: "Displays a ring showing the percentage of monitored containers with a pending update",
|
||||
values: { type: "boolean" },
|
||||
defaultValue: "yes",
|
||||
},
|
||||
{
|
||||
name: "Layout",
|
||||
description: "Arrange the two stats side by side (Horizontal) or stacked (Vertical)",
|
||||
values: { type: "select", options: ["Horizontal", "Vertical"] },
|
||||
defaultValue: "Horizontal",
|
||||
},
|
||||
{
|
||||
name: "Show container update list",
|
||||
description:
|
||||
"Lists each monitored container that has an image update available, linking to its release notes when known",
|
||||
values: { type: "boolean" },
|
||||
defaultValue: "no",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"label": "Workshop",
|
||||
"collapsed": true,
|
||||
"position": 4,
|
||||
"link": {
|
||||
"type": "doc",
|
||||
"id": "workshop/index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: Workshop development
|
||||
sidebar_position: 4
|
||||
tags:
|
||||
- Workshop
|
||||
- Developer
|
||||
- Docusaurus
|
||||
- PocketBase
|
||||
---
|
||||
|
||||
# Workshop development
|
||||
|
||||
Install the repository dependencies and make sure Docker is available before starting the Workshop services. See the
|
||||
[main development guide](../advanced/development/getting-started) for the repository prerequisites.
|
||||
|
||||
## Run PocketBase and documentation
|
||||
|
||||
Start PocketBase in one terminal:
|
||||
|
||||
```sh
|
||||
cp apps/workshop/.env.example apps/workshop/.env
|
||||
docker compose --env-file apps/workshop/.env -f apps/workshop/docker-compose.yml up --build workshop
|
||||
```
|
||||
|
||||
Then start Docusaurus in another terminal:
|
||||
|
||||
```sh
|
||||
WORKSHOP_API_URL=http://127.0.0.1:8090 pnpm dev:docs
|
||||
```
|
||||
|
||||
PocketBase runs on port 8090 and Docusaurus on port 3003. Stopping the services preserves the PocketBase volume.
|
||||
|
||||
Open `http://127.0.0.1:8090/_/` and create the first PocketBase superuser. The Workshop UI is available at
|
||||
`http://127.0.0.1:3003/workshop`.
|
||||
|
||||
Point Homarr at the local services with:
|
||||
|
||||
```dotenv
|
||||
HOMARR_WEBSITE_URL=http://127.0.0.1:3003
|
||||
WORKSHOP_API_URL=http://127.0.0.1:8090
|
||||
WORKSHOP_WEB_URL=http://127.0.0.1:3003/workshop
|
||||
```
|
||||
|
||||
Restart `pnpm dev:docs` after changing the Docusaurus URL variables. If Homarr is also running, restart its `pnpm dev`
|
||||
process after changing the Workshop URLs. GitHub OAuth is optional locally. See
|
||||
[`apps/workshop/README.md`](https://github.com/homarr-labs/homarr/blob/HEAD/apps/workshop/README.md) for OAuth,
|
||||
Workshop moderator promotion, logs, tests, backup, restore, stopping, and intentional volume reset.
|
||||
|
||||
## URL testing
|
||||
|
||||
Docusaurus reads `HOMARR_WEBSITE_URL` and `WORKSHOP_API_URL` when it starts or builds. Homarr also reads
|
||||
`WORKSHOP_WEB_URL` when its server starts, so these values do not rely on `NEXT_PUBLIC_*` build variables. Test a
|
||||
separate API origin before release:
|
||||
|
||||
```sh
|
||||
HOMARR_WEBSITE_URL=http://127.0.0.1:3003 \
|
||||
WORKSHOP_API_URL=http://127.0.0.1:8090 \
|
||||
WORKSHOP_WEB_URL=http://127.0.0.1:3003/workshop \
|
||||
pnpm dev:docs
|
||||
```
|
||||
|
||||
The [Workshop operator guide](./operator) covers production environment variables, staging, and service validation.
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
title: Homarr provider
|
||||
sidebar_position: 2
|
||||
tags:
|
||||
- Assistant
|
||||
- AI
|
||||
- Privacy
|
||||
- Community
|
||||
---
|
||||
|
||||
import { OpenCollectiveLink } from "@site/src/components/assistant-provider/open-collective-link";
|
||||
|
||||
# Homarr provider
|
||||
|
||||
The Homarr provider gives Community Workshop users access to the **Homarr model** without an administrator API key.
|
||||
The Homarr team chooses the upstream model based on quality, reliability, and cost.
|
||||
|
||||
## Set it up
|
||||
|
||||
1. Open **Management → Assistant**.
|
||||
2. Select **Homarr** as the provider.
|
||||
3. Save the provider connection.
|
||||
4. Select the available model, enable the assistant, and save.
|
||||
5. Open the assistant and sign in to the Workshop when asked.
|
||||
|
||||
## Daily allowance
|
||||
|
||||
- Each user gets **50 request units per UTC day**.
|
||||
- Each model request uses one unit. A tool loop can use several units.
|
||||
- Forwarded requests count even if the model later fails.
|
||||
- Allowances reset at 00:00 UTC. A shared daily limit also applies.
|
||||
|
||||
The battery beside the model selector shows the remaining allowance and reset time. Workshop enforces all limits on
|
||||
its server; changing the browser clock cannot bypass them.
|
||||
|
||||
## Privacy
|
||||
|
||||
Workshop keeps the OpenRouter API key on its server. Homarr and the browser never receive it.
|
||||
|
||||
Prompts, attachments, tools, and tool results pass through Workshop and OpenRouter. Workshop does not retain this
|
||||
content or the responses. It stores only the user, UTC day, and counters needed to enforce allowances. OpenRouter is
|
||||
configured for zero data retention with data collection disabled. Homarr does not use this content to train models.
|
||||
|
||||
We would rather become millionaires in a more ethical manner.
|
||||
|
||||
The community pays for these requests. Donations help cover the cost.
|
||||
|
||||
<OpenCollectiveLink />
|
||||
|
||||
Workshop operators can change the upstream model, per-user allowance, global allowance, and security limits in the
|
||||
[Workshop operator guide](./operator#homarr-provider-proxy).
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: Community Workshop
|
||||
sidebar_label: Overview
|
||||
tags:
|
||||
- Workshop
|
||||
- Custom Widgets
|
||||
- Custom CSS
|
||||
---
|
||||
|
||||
# Community Workshop
|
||||
|
||||
The [Community Workshop](pathname:///workshop) is where Homarr users share Custom Widgets and dashboard CSS.
|
||||
|
||||
:::warning Experimental
|
||||
|
||||
Workshop content is made by the community. Review it before installation and back up your Homarr instance.
|
||||
|
||||
:::
|
||||
|
||||
## Find and install content
|
||||
|
||||
Browse the Workshop by type, date, popularity, or recent activity. Each submission includes its source, screenshots,
|
||||
author, revision, comments, and reports.
|
||||
|
||||
Administrators can install content without leaving Homarr:
|
||||
|
||||
- **Custom Widgets:** open **Management → Custom Widgets**, then select **Import from Workshop**.
|
||||
- **Custom CSS:** open a board's **Settings → Custom CSS**, then select **Import from Workshop**.
|
||||
|
||||
Homarr shows the complete source and required permissions before installation. Widget credentials are entered in your
|
||||
Homarr instance and are never included in a Workshop submission.
|
||||
|
||||
## Share your work
|
||||
|
||||
1. Sign in to the Workshop with GitHub.
|
||||
2. Select **Share yours**.
|
||||
3. Choose **Custom Widget** or **Custom CSS**.
|
||||
4. Add the source, a short description, screenshots, and a changelog.
|
||||
5. Review and publish the submission.
|
||||
|
||||
Export Custom Widget JSON from **Management → Custom Widgets → Export**. You can edit a submission later without
|
||||
changing its Workshop URL.
|
||||
|
||||
## Voting, comments, and reports
|
||||
|
||||
Signed-in users can vote, comment, and report submissions. Authors can edit their own comments and submissions.
|
||||
Workshop moderators can review reports, remove comments, and delete unsafe content. Reports do not remove a submission
|
||||
automatically.
|
||||
|
||||
A Workshop moderator manages the public community. This is separate from the administrator role on a Homarr instance.
|
||||
|
||||
Deployment and server configuration are in the [Workshop operator guide](./operator). Local setup is in
|
||||
[Workshop development](./development).
|
||||
@@ -0,0 +1,144 @@
|
||||
---
|
||||
title: Workshop operator guide
|
||||
sidebar_label: Operator guide
|
||||
sidebar_position: 3
|
||||
tags:
|
||||
- Workshop
|
||||
- Deployment
|
||||
- Environment variables
|
||||
- PocketBase
|
||||
---
|
||||
|
||||
# Workshop operator guide
|
||||
|
||||
Workshop is powered by [PocketBase](https://pocketbase.io). The production image compiles the current Docusaurus site
|
||||
and serves it together with the API from one origin. The deployment lives in `apps/workshop/` with a Dockerfile,
|
||||
Compose configuration, version-controlled migrations, hooks, a smoke test, and ACT-local workflow instructions.
|
||||
|
||||
Submission links include Open Graph and X/Twitter metadata for the submission title, description, and type. The first
|
||||
uploaded screenshot is used as the social preview image; submissions without screenshots use the Homarr logo.
|
||||
|
||||
Run `pnpm docker:workshop` for PocketBase on port 8090, then run
|
||||
`WORKSHOP_API_URL=http://127.0.0.1:8090 pnpm dev:docs` for Docusaurus on port 3003. `pnpm docker:docs` builds the
|
||||
combined production layout. Normal start and stop commands retain data; `down --volumes` is an intentional,
|
||||
irreversible development reset. See [Workshop development](./development) for the complete local setup.
|
||||
|
||||
## Authentication and notifications
|
||||
|
||||
GitHub needs only the central callback `https://<workshop-host>/api/oauth2-redirect`. Keep PocketBase origins
|
||||
unrestricted unless every self-hosted Homarr caller is explicitly listed. Configure both `GITHUB_CLIENT_ID` and
|
||||
`GITHUB_CLIENT_SECRET`; supplying only one is a startup error. Set `WORKSHOP_PUBLIC_ORIGIN` in production so generated
|
||||
links and email use the public host. Workshop stores only the OAuth provider username in `users.name`, and users cannot
|
||||
rename it. Map **OAuth2 username** to `name`; leave OAuth2 id, full name, and avatar mappings empty. Profile and avatar
|
||||
URLs are derived from `https://github.com/<username>` and `https://github.com/<username>.png`, so PocketBase does not
|
||||
store duplicate profile metadata or avatar files. Configure SMTP for comment, report, and removal notices.
|
||||
|
||||
## Environment variables
|
||||
|
||||
The public URL contract and Workshop-only server configuration are:
|
||||
|
||||
| Variable | Purpose | Default |
|
||||
| -------------------------------------- | ------------------------------------------------- | ------------------------------ |
|
||||
| `HOMARR_WEBSITE_URL` | Documentation/site base used by Homarr links | `https://homarr.dev` |
|
||||
| `WORKSHOP_API_URL` | PocketBase API used by Homarr and standalone docs | `HOMARR_WEBSITE_URL` |
|
||||
| `WORKSHOP_WEB_URL` | Public Workshop page linked from Homarr | `HOMARR_WEBSITE_URL/workshop` |
|
||||
| `WORKSHOP_PUBLIC_ORIGIN` | PocketBase-generated links and email | Required in production |
|
||||
| `PB_ALLOWED_ORIGINS` | PocketBase browser origins | `*` for the central community |
|
||||
| `OPENROUTER_API_KEY` | Server-only key for the Homarr provider proxy | Provider disabled when empty |
|
||||
| `HOMARR_AI_DAILY_REQUEST_LIMIT` | Per-user daily provider allowance | `50` |
|
||||
| `HOMARR_AI_GLOBAL_DAILY_REQUEST_LIMIT` | Shared daily ceiling across all users | `10000` |
|
||||
| `HOMARR_AI_OPENROUTER_BASE_URL` | OpenRouter-compatible proxy upstream | `https://openrouter.ai/api/v1` |
|
||||
| `HOMARR_AI_OPENROUTER_MODEL` | Server-selected upstream model | Selected by the Homarr team |
|
||||
|
||||
All public URL variables reject non-HTTP protocols, embedded credentials, query strings, and fragments, and normalize
|
||||
trailing slashes when Homarr or Docusaurus starts. The deprecated docs-only `WORKSHOP_URL` alias remains for one
|
||||
release; `WORKSHOP_API_URL` takes precedence. The combined Workshop image writes the public URLs into its browser
|
||||
runtime configuration whenever the container starts; the Docker build arguments remain fallbacks. Restart the
|
||||
container after changing these values, without rebuilding it.
|
||||
|
||||
### Homarr provider proxy
|
||||
|
||||
The [Homarr provider](./homarr-provider) proxy is part of the Workshop image. `OPENROUTER_API_KEY` enables the proxy,
|
||||
`HOMARR_AI_OPENROUTER_BASE_URL` selects its OpenRouter-compatible upstream, `HOMARR_AI_OPENROUTER_MODEL` selects the
|
||||
private upstream model behind the stable `homarr/model` alias, and the two allowance variables provide per-user and
|
||||
shared request ceilings. Lowering either allowance takes effect for existing users. The upstream URL cannot contain
|
||||
embedded credentials, a query, or a fragment and must use HTTPS. Tests can explicitly opt into an insecure local mock
|
||||
with `HOMARR_AI_ALLOW_INSECURE_UPSTREAM=true`; do not use that setting with a real API key.
|
||||
|
||||
The proxy overwrites client routing and generation limits. It requires OpenRouter zero-data-retention routing, denies
|
||||
data-collecting providers, limits responses to 32,768 completion tokens, and does not retain prompts, responses, or
|
||||
per-request telemetry. It accepts only uploaded image data, conservatively bounds text to the advertised context, and
|
||||
allows at most two concurrent requests per user and 100 across the service. Keep an OpenRouter account-level credit
|
||||
limit as the final monetary backstop because request quotas are not dollar accounting.
|
||||
|
||||
Run `pnpm test:workshop` to test authentication, streaming, UTC reset, atomic per-user and global quota enforcement,
|
||||
failed-request accounting, identity attribution, server-selected model routing, function tools, input and output
|
||||
controls, privacy routing, spoofing resistance, absence of request logs, and migration rollback.
|
||||
|
||||
An operator can explicitly smoke-test a deployed Workshop against its real configured upstream with a dedicated,
|
||||
low-quota test user:
|
||||
|
||||
```sh
|
||||
HOMARR_AI_LIVE_SMOKE=1 \
|
||||
WORKSHOP_LIVE_TEST_URL=https://workshop.example.com \
|
||||
WORKSHOP_LIVE_TEST_TOKEN=<temporary-pocketbase-user-token> \
|
||||
pnpm test:workshop-live-provider
|
||||
```
|
||||
|
||||
This optional check consumes one provider request. The test client needs neither the OpenRouter key nor the private
|
||||
upstream model ID. Do not run it in CI or with a normal user's Workshop token.
|
||||
|
||||
The Custom Widget AI tests can use this proxy. Set `AI_PROVIDER_BASE_URL` to the `/api/ai/v1` endpoint and
|
||||
`AI_PROVIDER_API_KEY` to a temporary Workshop user token. See
|
||||
[Agent authoring](../management/custom-widgets/agent-authoring#evaluate-the-copied-prompt) for the command.
|
||||
|
||||
## Staging and demo deployment
|
||||
|
||||
Use `apps/workshop/docker-compose.staging.yml` with the published `ghcr.io/homarr-labs/workshop:v2` image for a v2
|
||||
preview. Copy `apps/workshop/.env.staging.example` to `.env.staging`; its default public contract is:
|
||||
|
||||
```dotenv
|
||||
HOMARR_WEBSITE_URL=https://v2.preview.homarr.dev
|
||||
WORKSHOP_API_URL=https://v2.preview.homarr.dev
|
||||
WORKSHOP_WEB_URL=https://v2.preview.homarr.dev/workshop
|
||||
WORKSHOP_PUBLIC_ORIGIN=https://v2.preview.homarr.dev
|
||||
PB_ALLOWED_ORIGINS=*
|
||||
```
|
||||
|
||||
Start or update it from the repository root:
|
||||
|
||||
```sh
|
||||
docker compose --env-file apps/workshop/.env.staging \
|
||||
-f apps/workshop/docker-compose.staging.yml pull
|
||||
docker compose --env-file apps/workshop/.env.staging \
|
||||
-f apps/workshop/docker-compose.staging.yml up --detach
|
||||
curl --fail http://127.0.0.1:8090/api/health
|
||||
```
|
||||
|
||||
The service binds to `127.0.0.1:8090`. Terminate TLS at a reverse proxy and forward the **entire hostname**, including
|
||||
`/api`, `/_/`, and `/workshop-runtime-config.js`; forwarding only `/workshop` breaks PocketBase and runtime URL loading.
|
||||
This combined image serves both the documentation site and PocketBase, so use a dedicated Workshop hostname if the
|
||||
chosen hostname already serves Homarr itself. In that case, replace the hostname in all four URL values and keep
|
||||
`/workshop` only on `WORKSHOP_WEB_URL`.
|
||||
|
||||
The Compose volume preserves PocketBase data across pulls and container recreation. The mutable `v2` tag is suitable
|
||||
for previews; use the `sha-<commit>` tag for promotion. For the public central Workshop, `PB_ALLOWED_ORIGINS=*` allows
|
||||
self-hosted Homarr origins to use the API and OAuth. A private demo may use a comma-separated origin allowlist instead.
|
||||
GitHub OAuth requires both credentials and the callback `https://v2.preview.homarr.dev/api/oauth2-redirect`. Restart
|
||||
after changing environment values, then verify the health endpoint, `/workshop`, a public listing request, GitHub
|
||||
sign-in, and an installation from the Homarr preview.
|
||||
|
||||
Back up application and PocketBase data before deployment. Publish the combined image by immutable digest, test the
|
||||
exact digest in staging, rehearse application rollback and PocketBase restore, then promote that same digest.
|
||||
|
||||
## Client architecture
|
||||
|
||||
Workshop access is centralized in `@homarr/workshop/backend`. It exports the typed `WorkshopBackend`, its typed
|
||||
PocketBase instance, stable TanStack Query option factories, and the `useWorkshop…` query and mutation hooks used by
|
||||
Homarr. The documentation site uses the same backend rather than maintaining a second API contract.
|
||||
|
||||
Listing fields derived by the `workshop_listings` view—such as submission type, score, and counts—are normalized by the
|
||||
shared client. Healthy deployments keep filtering, sorting, and pagination in PocketBase. If a persistent development
|
||||
volume still has an older view that rejects those fields, the client retries without them and applies the same
|
||||
operations locally; it can also fall back to the base submissions collection. Production deployments should
|
||||
nevertheless apply the current migration so the view exposes the complete metadata.
|
||||
@@ -362,7 +362,7 @@ html[data-workshop-detail-loading] #workshop-detail-route-fallback {
|
||||
box-sizing: border-box;
|
||||
color: #4b5563;
|
||||
display: flex;
|
||||
font: 500 0.875rem/1.5 Inter, system-ui, sans-serif;
|
||||
font: 500 0.875rem/1.5 "Work Sans", system-ui, sans-serif;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
@@ -402,6 +402,7 @@ html[data-workshop-detail-loading] #workshop-detail-route-fallback {
|
||||
path: "/workshop/:id",
|
||||
component: "@site/src/components/workshop/WorkshopDetailRoutePage",
|
||||
exact: true,
|
||||
priority: -1,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -51,7 +51,6 @@
|
||||
"postcss": "^8.5.16",
|
||||
"posthog-docusaurus": "^2.0.5",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"random-word-slugs": "^0.1.7",
|
||||
"react": "catalog:",
|
||||
"react-day-picker": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
.link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.45rem 0.7rem;
|
||||
border: 1px solid var(--ifm-color-emphasis-300);
|
||||
border-radius: 0.5rem;
|
||||
background: var(--ifm-background-surface-color);
|
||||
color: var(--ifm-font-color-base);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
background-color 150ms ease,
|
||||
border-color 150ms ease;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
border-color: var(--ifm-color-emphasis-500);
|
||||
background: var(--ifm-color-emphasis-100);
|
||||
color: var(--ifm-font-color-base);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.link:focus-visible {
|
||||
outline: 2px solid var(--ifm-color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.link {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import Translate from "@docusaurus/Translate";
|
||||
|
||||
import styles from "./open-collective-link.module.css";
|
||||
|
||||
export const OpenCollectiveLink = () => (
|
||||
<a className={styles.link} href="https://opencollective.com/homarr">
|
||||
<img className={styles.logo} src="/img/opencollective.svg" alt="" aria-hidden />
|
||||
<Translate id="workshop.provider.donate">Donate to Homarr on OpenCollective</Translate>
|
||||
</a>
|
||||
);
|
||||
@@ -1,22 +1,100 @@
|
||||
import { translate } from "@docusaurus/Translate";
|
||||
import { IconBrandDocker, IconCpu, IconDeviceDesktopAnalytics } from "@tabler/icons-react";
|
||||
|
||||
import styles from "../../../../pages/index.module.css";
|
||||
import { AppWidget } from "./widgets/app-widget";
|
||||
import { ClockWidget } from "./widgets/clock-widget";
|
||||
import { DownloadsWidget } from "./widgets/downloads-widget";
|
||||
import { WeatherWidget } from "./widgets/weather-widget";
|
||||
import { StockWidget } from "./widgets/stock-widget";
|
||||
import { EntityStateWidget } from "./widgets/entity-state-widget";
|
||||
import { StockWidget } from "./widgets/stock-widget";
|
||||
import { WeatherWidget } from "./widgets/weather-widget";
|
||||
import { WidgetCard } from "./widgets/card";
|
||||
|
||||
export const HeroCards = () => {
|
||||
return (
|
||||
<div className="argos-ignore hero-cards flex flex-wrap max-w-[504px] gap-y-4 gap-x-3 text-gray-700 dark:text-gray-300">
|
||||
<div className={`argos-ignore hero-cards ${styles.heroCards}`}>
|
||||
<StockWidget />
|
||||
<AppWidget />
|
||||
<AppWidget className="hidden 3xl:block" />
|
||||
<WeatherWidget />
|
||||
<DownloadsWidget />
|
||||
<EntityStateWidget />
|
||||
<AppWidget />
|
||||
|
||||
<AppWidget name="Plex" />
|
||||
<AppWidget name="Jellyfin" />
|
||||
<AppWidget name="Homeassistant" label="Home Assistant" />
|
||||
<ClockWidget />
|
||||
<SystemResourcesWidget />
|
||||
<WeatherWidget />
|
||||
<EntityStateWidget />
|
||||
<AppWidget name="PiHole" label="Pi-hole" />
|
||||
<AppWidget name="qBittorrent" />
|
||||
<DownloadsWidget />
|
||||
<DockerWidget />
|
||||
<AppWidget name="Sonarr" />
|
||||
<AppWidget name="Radarr" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const heroLabels = {
|
||||
systemResources: translate({ id: "homepage.preview.systemResources", message: "System resources" }),
|
||||
online: translate({ id: "homepage.preview.online", message: "Online" }),
|
||||
cpu: translate({ id: "homepage.preview.cpu", message: "CPU" }),
|
||||
memory: translate({ id: "homepage.preview.memory", message: "Memory" }),
|
||||
disk: translate({ id: "homepage.preview.disk", message: "Disk" }),
|
||||
docker: translate({ id: "homepage.preview.docker", message: "Docker" }),
|
||||
dockerSummary: translate({ id: "homepage.preview.dockerSummary", message: "2 running · 1 failed" }),
|
||||
healthy: translate({ id: "homepage.preview.healthy", message: "Healthy" }),
|
||||
running: translate({ id: "homepage.preview.running", message: "Running" }),
|
||||
exited: translate({ id: "homepage.preview.exited", message: "Exited" }),
|
||||
};
|
||||
|
||||
const SystemResourcesWidget = () => (
|
||||
<WidgetCard width={2} className={styles.resourcesWidget}>
|
||||
<div className={styles.widgetHeading}>
|
||||
<span className={styles.widgetTitle}>
|
||||
<IconDeviceDesktopAnalytics size={17} aria-hidden="true" />
|
||||
{heroLabels.systemResources}
|
||||
</span>
|
||||
<span className={styles.onlineStatus}>{heroLabels.online}</span>
|
||||
</div>
|
||||
<ResourceMeter icon={<IconCpu size={15} aria-hidden="true" />} label={heroLabels.cpu} value={34} />
|
||||
<ResourceMeter label={heroLabels.memory} value={62} />
|
||||
<ResourceMeter label={heroLabels.disk} value={98} />
|
||||
</WidgetCard>
|
||||
);
|
||||
|
||||
const ResourceMeter = ({ icon, label, value }: { icon?: React.ReactNode; label: string; value: number }) => (
|
||||
<div className={`${styles.resourceMeter} ${value >= 90 ? styles.resourceMeterCritical : ""}`}>
|
||||
<div className={styles.resourceLabel}>
|
||||
<span>
|
||||
{icon}
|
||||
{label}
|
||||
</span>
|
||||
<span>{value}%</span>
|
||||
</div>
|
||||
<progress className={styles.resourceTrack} aria-label={`${label} usage`} max={100} value={value} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const DockerWidget = () => (
|
||||
<WidgetCard width={2} className={styles.dockerWidget}>
|
||||
<div className={styles.widgetHeading}>
|
||||
<span className={styles.widgetTitle}>
|
||||
<IconBrandDocker size={18} aria-hidden="true" />
|
||||
{heroLabels.docker}
|
||||
</span>
|
||||
<span className={styles.dockerCount}>{heroLabels.dockerSummary}</span>
|
||||
</div>
|
||||
<div className={styles.containerList}>
|
||||
{[
|
||||
{ name: "immich", status: heroLabels.healthy, failed: false },
|
||||
{ name: "jellyfin", status: heroLabels.running, failed: false },
|
||||
{ name: "paperless", status: heroLabels.exited, failed: true },
|
||||
].map(({ name, status, failed }) => (
|
||||
<div className={`${styles.containerRow} ${failed ? styles.containerRowFailed : ""}`} key={name}>
|
||||
<span className={styles.containerName}>{name}</span>
|
||||
<span className={styles.containerStatus}>
|
||||
<span className={`${styles.statusDot} ${failed ? styles.statusDotFailed : ""}`} aria-hidden="true" />
|
||||
{status}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</WidgetCard>
|
||||
);
|
||||
@@ -1,4 +1,5 @@
|
||||
import Link from "@docusaurus/Link";
|
||||
import { translate } from "@docusaurus/Translate";
|
||||
import { IconDownload, IconExternalLink } from "@tabler/icons-react";
|
||||
import clsx from "clsx";
|
||||
import styles from "../../../../pages/index.module.css";
|
||||
@@ -7,9 +8,9 @@ import { HeroCards } from "./hero-cards";
|
||||
export default function HomeHero() {
|
||||
return (
|
||||
<header className={clsx("hero hero--primary", styles.heroBanner, styles.hero)}>
|
||||
<div className="flex">
|
||||
<div className="ps-0 lg:ps-52">
|
||||
<h1 className="font-extrabold lg:text-7xl text-3xl">
|
||||
<div className={styles.heroContent}>
|
||||
<div className={styles.heroCopy}>
|
||||
<h1 className={clsx("font-extrabold lg:text-7xl text-3xl", styles.heroTitle)}>
|
||||
A simple, yet
|
||||
<br />
|
||||
powerful dashboard
|
||||
@@ -22,7 +23,7 @@ export default function HomeHero() {
|
||||
information.
|
||||
</p>
|
||||
|
||||
<div className={"flex flex-nowrap gap-2"}>
|
||||
<div className={styles.heroActions}>
|
||||
<Link
|
||||
data-attr="Install button"
|
||||
className={"button button--secondary button--lg rounded-3xl dark:border-zinc-600 dark:bg-zinc-800"}
|
||||
@@ -57,9 +58,12 @@ export default function HomeHero() {
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow-1 w-full lg:block hidden hover-animation">
|
||||
<section
|
||||
className={clsx(styles.heroPreview, "hover-animation")}
|
||||
aria-label={translate({ id: "homepage.preview.label", message: "Example Homarr dashboard" })}
|
||||
>
|
||||
<HeroCards />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { supportedIntegrations } from "@site/src/constants/supported-integrations";
|
||||
import { CommonWidgetProps, WidgetCard } from "./card";
|
||||
import { getRndInteger } from "@site/src/tools/math";
|
||||
import clsx from "clsx";
|
||||
|
||||
export const AppWidget = ({ className }: CommonWidgetProps) => {
|
||||
const randomApp = supportedIntegrations[getRndInteger(0, supportedIntegrations.length - 1)];
|
||||
interface AppWidgetProps extends CommonWidgetProps {
|
||||
name: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const AppWidget = ({ name, label, className }: AppWidgetProps) => {
|
||||
const app = supportedIntegrations.find((integration) => integration.name === name);
|
||||
if (!app) return null;
|
||||
|
||||
return (
|
||||
<WidgetCard width={1} className={clsx("text-center", className)}>
|
||||
<span className={"text-sm font-bold"}>{randomApp.name}</span>
|
||||
<img
|
||||
src={randomApp.iconUrl}
|
||||
className="aspect-square scale-[0.6] transition-transform"
|
||||
alt={`${randomApp.name} Icon`}
|
||||
/>
|
||||
<div className="absolute bottom-3 right-3 rounded-full bg-green-500 w-2 h-2"></div>
|
||||
<WidgetCard width={1} className={clsx("app-card text-center", className)}>
|
||||
<span className={"text-sm font-bold"}>{label ?? app.name}</span>
|
||||
<img src={app.iconUrl} className="aspect-square scale-[0.6]" alt="" />
|
||||
<div className="absolute bottom-3 right-3 rounded-full bg-green-500 w-2 h-2" aria-hidden="true" />
|
||||
</WidgetCard>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import clsx from "clsx";
|
||||
import { ReactNode } from "react";
|
||||
import styles from "../../../../../pages/index.module.css";
|
||||
|
||||
export interface CommonWidgetProps {
|
||||
className?: string;
|
||||
@@ -16,15 +17,13 @@ export const WidgetCard = ({ width, children, className, onClick }: WidgetCardPr
|
||||
return (
|
||||
<Component
|
||||
className={clsx(
|
||||
"rounded-xl flex flex-col justify-center items-center p-2 relative border-none bg-[var(--ifm-navbar-background-color)]",
|
||||
styles.widgetCard,
|
||||
"flex flex-col justify-center items-center p-2 relative border-none bg-[var(--ifm-navbar-background-color)]",
|
||||
className,
|
||||
onClick ? "cursor-pointer" : undefined,
|
||||
)}
|
||||
style={{
|
||||
width: (128 * width + (width === 2 ? 16 : 0)) * 0.9,
|
||||
height: 128,
|
||||
background: "rgb(from var(--ifm-navbar-background-color) r g b / 80%)",
|
||||
}}
|
||||
style={{ width: (128 * width + (width === 2 ? 16 : 0)) * 0.9, height: 128 }}
|
||||
type={onClick ? "button" : undefined}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,26 +1,40 @@
|
||||
import { getRndInteger } from "@site/src/tools/math";
|
||||
import { translate } from "@docusaurus/Translate";
|
||||
import { useEffect, useState } from "react";
|
||||
import { CommonWidgetProps, WidgetCard } from "./card";
|
||||
import clsx from "clsx";
|
||||
import { generateSlug } from "random-word-slugs";
|
||||
import styles from "../../../../../pages/index.module.css";
|
||||
|
||||
const downloadQueue = ["family-photos.zip", "linux-isos.mkv", "server-backup.zip", "holiday-video.mp4"];
|
||||
const downloadLabels = {
|
||||
file: translate({ id: "homepage.preview.file", message: "File" }),
|
||||
progress: translate({ id: "homepage.preview.progress", message: "Progress" }),
|
||||
};
|
||||
|
||||
export const DownloadsWidget = ({ className }: CommonWidgetProps) => {
|
||||
const [downloads, setDownloads] = useState<Download[]>([]);
|
||||
const [downloads, setDownloads] = useState<Download[]>([
|
||||
{ filename: "photos-backup.zip", progress: 72 },
|
||||
{ filename: "media-library.mkv", progress: 38 },
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const intervalId = setInterval(() => {
|
||||
setDownloads((prev: Download[]) => {
|
||||
const filteredPrev = prev.filter((download) => download.progress !== 100);
|
||||
|
||||
filteredPrev.forEach((download, index) => {
|
||||
download.progress = Math.min(100, download.progress + getRndInteger(0, index < 2 ? 16 : 8));
|
||||
});
|
||||
const progressedDownloads = filteredPrev.map((download, index) => ({
|
||||
...download,
|
||||
progress: Math.min(100, download.progress + (index < 2 ? 8 : 4)),
|
||||
}));
|
||||
|
||||
if (filteredPrev.length < 3 && filteredPrev.length === prev.length) {
|
||||
filteredPrev.push(generateRandomDownload());
|
||||
if (progressedDownloads.length < 3 && progressedDownloads.length === prev.length) {
|
||||
const availableDownloads = downloadQueue.filter(
|
||||
(filename) => !progressedDownloads.some((download) => download.filename === filename),
|
||||
);
|
||||
const filename = availableDownloads[0];
|
||||
if (filename) progressedDownloads.push({ filename, progress: 0 });
|
||||
}
|
||||
|
||||
return [...filteredPrev];
|
||||
return progressedDownloads;
|
||||
});
|
||||
}, 500);
|
||||
|
||||
@@ -34,19 +48,31 @@ export const DownloadsWidget = ({ className }: CommonWidgetProps) => {
|
||||
<table className="mb-0 w-full h-full text-xs">
|
||||
<thead className="inline-block w-full">
|
||||
<tr className="inline-block w-full">
|
||||
<th className="border-none px-2 py-1 text-start inline-block w-1/2">File</th>
|
||||
<th className="border-none px-2 py-1 text-start inline-block w-1/2">Progress</th>
|
||||
<th className="border-none px-2 py-1 text-start inline-block w-1/2">{downloadLabels.file}</th>
|
||||
<th className="border-none px-2 py-1 text-start inline-block w-1/2">{downloadLabels.progress}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{downloads.map((file: Download, index: number) => {
|
||||
{downloads.map((file: Download) => {
|
||||
return (
|
||||
<tr key={index}>
|
||||
<tr key={file.filename}>
|
||||
<td className="border-none text-nowrap p-2">{file.filename}</td>
|
||||
<td className="border-none p-2 w-full">
|
||||
<div className="overflow-hidden rounded-xl h-2 w-full bg-zinc-600">
|
||||
<div className="bg-green-600 h-full animated-width" style={{ width: `${file.progress}%` }}></div>
|
||||
</div>
|
||||
<td
|
||||
className="border-none p-2 w-full"
|
||||
aria-label={translate(
|
||||
{ id: "homepage.preview.downloadComplete", message: "{progress}% complete" },
|
||||
{ progress: file.progress },
|
||||
)}
|
||||
>
|
||||
<progress
|
||||
className={styles.downloadProgress}
|
||||
value={file.progress}
|
||||
max={100}
|
||||
aria-label={translate(
|
||||
{ id: "homepage.preview.downloadProgress", message: "{filename} download progress" },
|
||||
{ filename: file.filename },
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
@@ -61,15 +87,3 @@ interface Download {
|
||||
filename: string;
|
||||
progress: number;
|
||||
}
|
||||
|
||||
const formats = ["mkv", "mp4"];
|
||||
|
||||
const generateRandomDownload = (): Download => {
|
||||
const randomMovie = generateSlug(2);
|
||||
const randomFormat = formats[getRndInteger(0, formats.length - 1)];
|
||||
|
||||
return {
|
||||
filename: `${randomMovie}.${randomFormat}`,
|
||||
progress: 0,
|
||||
};
|
||||
};
|
||||
@@ -27,6 +27,7 @@ export const StockWidget = ({ className }: CommonWidgetProps) => {
|
||||
HOMR
|
||||
</div>
|
||||
<Line
|
||||
ariaLabel="Simulated HOMR stock price trend"
|
||||
height={128}
|
||||
width={244}
|
||||
colors={[upwardTrend ? "green" : "red"]}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { IconCheck, IconEdit, IconMessageCircle, IconRefresh, IconSend, IconTras
|
||||
|
||||
import type { WorkshopComment } from "@site/src/lib/pocketbase";
|
||||
import { getWorkshopBackend } from "@site/src/lib/pocketbase";
|
||||
import { githubAvatarUrl, githubProfileUrl } from "@homarr/workshop/schema";
|
||||
import type { WorkshopUser } from "@homarr/workshop/schema";
|
||||
import { errorMessage } from "@site/src/lib/utils";
|
||||
|
||||
@@ -158,13 +159,10 @@ export const CommentsSection = ({ submissionId, backend, currentUser, onRequireA
|
||||
const canDelete = ownComment || currentUser?.isAdmin === true;
|
||||
const isEditing = editingId === comment.id;
|
||||
const edited = comment.updated !== comment.created;
|
||||
const authorName = ownComment ? (currentUser?.displayName ?? comment.authorName) : comment.authorName;
|
||||
const avatarUrl = ownComment
|
||||
? (currentUser?.avatarUrl ?? comment.authorAvatarUrl)
|
||||
: comment.authorAvatarUrl;
|
||||
const profileUrl = ownComment
|
||||
? (currentUser?.githubProfileUrl ?? comment.authorGithubProfileUrl)
|
||||
: comment.authorGithubProfileUrl;
|
||||
const authorHandle = ownComment ? (currentUser?.name ?? comment.authorName) : comment.authorName;
|
||||
const authorName = authorHandle || "Community member";
|
||||
const avatarUrl = githubAvatarUrl(authorHandle);
|
||||
const profileUrl = githubProfileUrl(authorHandle);
|
||||
|
||||
return (
|
||||
<article
|
||||
@@ -292,10 +290,8 @@ export const CommentsSection = ({ submissionId, backend, currentUser, onRequireA
|
||||
<div className="rounded-xl border border-border bg-muted/30 p-3">
|
||||
<div className="flex items-start gap-2.5">
|
||||
<Avatar className="mt-0.5 size-9 bg-background ring-1 ring-border">
|
||||
{currentUser?.avatarUrl && <AvatarImage src={currentUser.avatarUrl} alt="" />}
|
||||
<AvatarFallback className="bg-background">
|
||||
{avatarFallback(currentUser?.displayName ?? "Guest")}
|
||||
</AvatarFallback>
|
||||
{currentUser?.name && <AvatarImage src={githubAvatarUrl(currentUser.name)} alt="" />}
|
||||
<AvatarFallback className="bg-background">{avatarFallback(currentUser?.name ?? "Guest")}</AvatarFallback>
|
||||
</Avatar>
|
||||
<Textarea
|
||||
className="min-h-16 flex-1 resize-none bg-background"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import BrowserOnly from "@docusaurus/BrowserOnly";
|
||||
import Head from "@docusaurus/Head";
|
||||
import Link from "@docusaurus/Link";
|
||||
import { useLocation } from "@docusaurus/router";
|
||||
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
||||
@@ -33,6 +34,9 @@ import { getRuntimeWorkshopApiUrl } from "@site/src/lib/runtime-config";
|
||||
import type { SubmissionType } from "@site/src/lib/workshop-schema";
|
||||
import type { HomarrCustomWidgetV2 } from "@homarr/custom-widgets/core";
|
||||
import {
|
||||
githubAvatarUrl,
|
||||
githubProfileUrl,
|
||||
formatWorkshopContent,
|
||||
MAX_WORKSHOP_SCREENSHOTS,
|
||||
MAX_WORKSHOP_SCREENSHOT_BYTES,
|
||||
WORKSHOP_SCREENSHOT_MIME_TYPES,
|
||||
@@ -66,6 +70,7 @@ import { WorkshopErrorBoundary } from "./WorkshopErrorBoundary";
|
||||
import { downloadSubmissionJson, voteAndReconcile } from "./workshop-utils";
|
||||
|
||||
const typeLabels: Record<SubmissionType, string> = { customCss: "CSS", customWidget: "Widget" };
|
||||
const typeSocialLabels: Record<SubmissionType, string> = { customCss: "Custom CSS", customWidget: "Custom widget" };
|
||||
const typeDotColors: Record<SubmissionType, string> = { customCss: "bg-blue-500", customWidget: "bg-yellow-500" };
|
||||
const contentLanguages: Record<SubmissionType, string> = { customCss: "css", customWidget: "json" };
|
||||
const copyState = [
|
||||
@@ -449,7 +454,7 @@ const MarketplaceDetail = ({ workshopUrl }: { workshopUrl: string }) => {
|
||||
try {
|
||||
await backend.report(
|
||||
submission.id,
|
||||
reportCategory as "malicious" | "spam" | "copyright" | "inappropriate" | "other",
|
||||
reportCategory as "outdated" | "malicious" | "spam" | "copyright" | "inappropriate" | "other",
|
||||
reportExplanation,
|
||||
);
|
||||
setReportOpen(false);
|
||||
@@ -524,17 +529,40 @@ const MarketplaceDetail = ({ workshopUrl }: { workshopUrl: string }) => {
|
||||
const canManage = user?.id === submission.author || user?.isAdmin === true;
|
||||
const canEdit = user?.id === submission.author;
|
||||
const signedInAuthor = user?.id === submission.author ? user : null;
|
||||
const authorName = signedInAuthor?.displayName || submission.authorName;
|
||||
const authorAvatarUrl = signedInAuthor?.avatarUrl || submission.authorAvatarUrl;
|
||||
const authorGithubUsername = signedInAuthor?.githubUsername || submission.authorGithubUsername;
|
||||
const authorGithubProfileUrl = signedInAuthor?.githubProfileUrl || submission.authorGithubProfileUrl;
|
||||
const authorHandle = signedInAuthor?.name || submission.authorName;
|
||||
const authorName = authorHandle || "Community member";
|
||||
const authorAvatarUrl = githubAvatarUrl(authorHandle);
|
||||
const authorGithubProfileUrl = githubProfileUrl(authorHandle);
|
||||
const sources = widgetDefinition ? Object.entries(widgetDefinition.sources) : [];
|
||||
const requests = widgetDefinition ? Object.entries(widgetDefinition.requests) : [];
|
||||
const options = widgetDefinition ? Object.entries(widgetDefinition.options) : [];
|
||||
const protectedSources = sources.filter(([, source]) => sourceNeedsSecret(source.auth));
|
||||
const displayedContent = formatWorkshopContent(submission.type, submission.content);
|
||||
const socialTitle = `${submission.title} · Homarr Workshop`;
|
||||
const socialDescription = `${typeSocialLabels[submission.type]} for Homarr. ${submission.description}`.trim();
|
||||
const socialUrl = `${window.location.origin}${window.location.pathname}`;
|
||||
const socialImage = screenshotUrls[0] ?? `${window.location.origin}/img/logo.png`;
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-[90rem] px-4 pb-20 pt-8 sm:px-6 lg:px-8">
|
||||
<Head>
|
||||
<title>{socialTitle}</title>
|
||||
<meta name="description" content={socialDescription} />
|
||||
<link rel="canonical" href={socialUrl} />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:site_name" content="Homarr Workshop" />
|
||||
<meta property="og:title" content={socialTitle} />
|
||||
<meta property="og:description" content={socialDescription} />
|
||||
<meta property="og:url" content={socialUrl} />
|
||||
<meta property="og:image" content={socialImage} />
|
||||
<meta property="og:image:alt" content={`${submission.title} preview`} />
|
||||
<meta property="article:section" content={typeSocialLabels[submission.type]} />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={socialTitle} />
|
||||
<meta name="twitter:description" content={socialDescription} />
|
||||
<meta name="twitter:image" content={socialImage} />
|
||||
<meta name="twitter:image:alt" content={`${submission.title} preview`} />
|
||||
</Head>
|
||||
<Toaster position="bottom-right" richColors />
|
||||
<Link
|
||||
to="/workshop"
|
||||
@@ -593,10 +621,7 @@ const MarketplaceDetail = ({ workshopUrl }: { workshopUrl: string }) => {
|
||||
{authorName}
|
||||
{authorGithubProfileUrl && <IconBrandGithub size={14} />}
|
||||
</a>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{authorGithubUsername ? `@${authorGithubUsername} · ` : ""}
|
||||
Published {formatRelativeTime(submission.created)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">Published {formatRelativeTime(submission.created)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -698,12 +723,12 @@ const MarketplaceDetail = ({ workshopUrl }: { workshopUrl: string }) => {
|
||||
<CustomWidgetCodeExample
|
||||
id={`workshop-${submission.id}`}
|
||||
label="widget.json"
|
||||
code={submission.content}
|
||||
code={displayedContent}
|
||||
language="json"
|
||||
height="min(58vh, 640px)"
|
||||
/>
|
||||
) : (
|
||||
<CodeBlock content={submission.content} language={contentLanguages[submission.type]} />
|
||||
<CodeBlock content={displayedContent} language={contentLanguages[submission.type]} />
|
||||
)}
|
||||
</section>
|
||||
|
||||
@@ -733,7 +758,7 @@ const MarketplaceDetail = ({ workshopUrl }: { workshopUrl: string }) => {
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
to="/docs/management/workshop/"
|
||||
to="/docs/workshop/"
|
||||
className="mt-4 inline-flex items-center gap-1.5 text-sm font-medium text-primary hover:underline"
|
||||
>
|
||||
Installation guide <IconExternalLink size={13} />
|
||||
@@ -882,6 +907,7 @@ const MarketplaceDetail = ({ workshopUrl }: { workshopUrl: string }) => {
|
||||
<SelectValue>{(value) => String(value).replace(/^./u, (letter) => letter.toUpperCase())}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent align="start">
|
||||
<SelectItem value="outdated">Outdated or no longer working</SelectItem>
|
||||
<SelectItem value="malicious">Malicious</SelectItem>
|
||||
<SelectItem value="spam">Spam</SelectItem>
|
||||
<SelectItem value="copyright">Copyright</SelectItem>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { IconBrandGithub, IconChevronDown, IconExternalLink, IconLogout } from "@tabler/icons-react";
|
||||
|
||||
import { githubAvatarUrl, githubProfileUrl } from "@homarr/workshop/schema";
|
||||
import type { WorkshopUser } from "@homarr/workshop/schema";
|
||||
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
@@ -9,10 +10,6 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
const avatarFallback = (name: string) => name.trim().slice(0, 1).toUpperCase() || "?";
|
||||
|
||||
const githubProfileUrl = (user: WorkshopUser) =>
|
||||
user.githubProfileUrl ||
|
||||
(user.githubUsername ? `https://github.com/${encodeURIComponent(user.githubUsername)}` : undefined);
|
||||
|
||||
interface WorkshopAccountMenuProps {
|
||||
user: WorkshopUser;
|
||||
onSignOut: () => void;
|
||||
@@ -21,8 +18,9 @@ interface WorkshopAccountMenuProps {
|
||||
export const WorkshopAccountMenu = ({ user, onSignOut }: WorkshopAccountMenuProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const profileUrl = githubProfileUrl(user);
|
||||
const accountLabel = user.githubUsername ? `@${user.githubUsername}` : user.displayName || "Account";
|
||||
const profileUrl = githubProfileUrl(user.name);
|
||||
const avatarUrl = githubAvatarUrl(user.name);
|
||||
const accountLabel = user.name ? `@${user.name}` : "Account";
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
@@ -54,8 +52,8 @@ export const WorkshopAccountMenu = ({ user, onSignOut }: WorkshopAccountMenuProp
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
>
|
||||
<Avatar className="size-6">
|
||||
{user.avatarUrl && <AvatarImage src={user.avatarUrl} alt="" />}
|
||||
<AvatarFallback className="text-[10px]">{avatarFallback(user.displayName)}</AvatarFallback>
|
||||
{avatarUrl && <AvatarImage src={avatarUrl} alt="" />}
|
||||
<AvatarFallback className="text-[10px]">{avatarFallback(user.name)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="max-w-36 truncate">{accountLabel}</span>
|
||||
<IconChevronDown
|
||||
@@ -73,12 +71,11 @@ export const WorkshopAccountMenu = ({ user, onSignOut }: WorkshopAccountMenuProp
|
||||
>
|
||||
<div className="flex items-center gap-3 px-2 py-2">
|
||||
<Avatar className="size-9">
|
||||
{user.avatarUrl && <AvatarImage src={user.avatarUrl} alt="" />}
|
||||
<AvatarFallback>{avatarFallback(user.displayName)}</AvatarFallback>
|
||||
{avatarUrl && <AvatarImage src={avatarUrl} alt="" />}
|
||||
<AvatarFallback>{avatarFallback(user.name)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium">{user.displayName}</p>
|
||||
{user.githubUsername && <p className="truncate text-xs text-muted-foreground">@{user.githubUsername}</p>}
|
||||
<p className="truncate text-sm font-medium">{accountLabel}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
import { WorkshopBackend } from "@homarr/workshop/backend";
|
||||
import { githubAvatarUrl, githubProfileUrl } from "@homarr/workshop/schema";
|
||||
import type { WorkshopReport, WorkshopSubmissionSummary, WorkshopUser } from "@homarr/workshop/schema";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
@@ -108,7 +109,11 @@ export function WorkshopAdmin({ workshopUrl }: { workshopUrl?: string }) {
|
||||
submission,
|
||||
reports: reports.filter((report) => report.submission === submission.id),
|
||||
}))
|
||||
.sort((left, right) => Number(right.reports.length > 0) - Number(left.reports.length > 0));
|
||||
.sort(
|
||||
(left, right) =>
|
||||
right.reports.length - left.reports.length ||
|
||||
Date.parse(right.submission.updated) - Date.parse(left.submission.updated),
|
||||
);
|
||||
|
||||
return (
|
||||
<main className="marketplace mx-auto max-w-6xl px-4 pb-20 text-foreground sm:px-6">
|
||||
@@ -194,18 +199,18 @@ export function WorkshopAdmin({ workshopUrl }: { workshopUrl?: string }) {
|
||||
<CardHeader className="border-b border-border pb-4 sm:grid-cols-[1fr_auto]">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<Avatar className="size-10">
|
||||
{submission.authorAvatarUrl && <AvatarImage src={submission.authorAvatarUrl} alt="" />}
|
||||
{submission.authorName && <AvatarImage src={githubAvatarUrl(submission.authorName)} alt="" />}
|
||||
<AvatarFallback>{avatarFallback(submission.authorName)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0">
|
||||
<CardTitle className="truncate text-base">{submission.title}</CardTitle>
|
||||
<a
|
||||
href={submission.authorGithubProfileUrl || undefined}
|
||||
target={submission.authorGithubProfileUrl ? "_blank" : undefined}
|
||||
href={githubProfileUrl(submission.authorName) || undefined}
|
||||
target={submission.authorName ? "_blank" : undefined}
|
||||
rel="noreferrer"
|
||||
className="text-xs text-muted-foreground hover:text-foreground hover:underline"
|
||||
>
|
||||
{submission.authorName}
|
||||
{submission.authorName || "Community member"}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -237,7 +242,8 @@ export function WorkshopAdmin({ workshopUrl }: { workshopUrl?: string }) {
|
||||
<div className="min-w-0">
|
||||
<AlertTitle className="col-auto capitalize">{report.category} report</AlertTitle>
|
||||
<p className="m-0 mt-1 text-xs text-destructive/75">
|
||||
Reported by {report.reporterName} · {new Date(report.created).toLocaleString()}
|
||||
Reported by {report.reporterName || "Community member"} ·{" "}
|
||||
{new Date(report.created).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
|
||||
@@ -8,16 +8,19 @@ import {
|
||||
IconChevronLeft,
|
||||
IconChevronRight,
|
||||
IconEye,
|
||||
IconFlag,
|
||||
IconMessage,
|
||||
IconPackage,
|
||||
IconPlus,
|
||||
IconPuzzle,
|
||||
IconRefresh,
|
||||
IconSearch,
|
||||
IconShield,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
import type { WorkshopSubmission } from "@site/src/lib/pocketbase";
|
||||
import type { SubmissionType } from "@site/src/lib/workshop-schema";
|
||||
import { githubAvatarUrl, githubProfileUrl } from "@homarr/workshop/schema";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
@@ -115,6 +118,7 @@ export const WorkshopApp = ({ workshopUrl }: { workshopUrl: string }) => {
|
||||
const availableTypeFilters = workshop.user
|
||||
? [...typeFilters, { value: "yours" as const, label: "Yours" }]
|
||||
: typeFilters;
|
||||
const openReportCount = workshop.submissions.reduce((total, submission) => total + submission.reportCount, 0);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl px-4 pb-16">
|
||||
@@ -125,9 +129,19 @@ export const WorkshopApp = ({ workshopUrl }: { workshopUrl: string }) => {
|
||||
Discover community-made widgets and CSS. Review the source, then import it into Homarr.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 self-start sm:self-auto">
|
||||
<div className="flex flex-wrap items-center gap-2 self-start sm:self-auto">
|
||||
{workshop.user ? (
|
||||
<>
|
||||
{workshop.user.isAdmin && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-10 sm:h-8"
|
||||
nativeButton={false}
|
||||
render={<a href="/workshop/admin" aria-label={`Open moderation, ${openReportCount} open reports`} />}
|
||||
>
|
||||
<IconShield size={14} /> Moderation{openReportCount > 0 ? ` (${openReportCount})` : ""}
|
||||
</Button>
|
||||
)}
|
||||
<Button className="h-10 sm:h-8" onClick={() => setShowSubmit(true)}>
|
||||
<IconPlus size={14} /> Share yours
|
||||
</Button>
|
||||
@@ -356,16 +370,16 @@ const SubmissionCard = ({ submission, backend, userVote, onVote }: SubmissionCar
|
||||
</div>
|
||||
<CardDescription className="flex min-w-0 items-center gap-1 text-xs">
|
||||
<a
|
||||
href={submission.authorGithubProfileUrl || undefined}
|
||||
target={submission.authorGithubProfileUrl ? "_blank" : undefined}
|
||||
href={githubProfileUrl(submission.authorName) || undefined}
|
||||
target={submission.authorName ? "_blank" : undefined}
|
||||
rel="noreferrer"
|
||||
className="inline-flex min-w-0 items-center gap-1.5 hover:text-foreground"
|
||||
>
|
||||
<Avatar className="size-4">
|
||||
{submission.authorAvatarUrl && <AvatarImage src={submission.authorAvatarUrl} alt="" />}
|
||||
{submission.authorName && <AvatarImage src={githubAvatarUrl(submission.authorName)} alt="" />}
|
||||
<AvatarFallback className="text-[9px]">{avatarFallback(submission.authorName)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="truncate">{submission.authorName}</span>
|
||||
<span className="truncate">{submission.authorName || "Community member"}</span>
|
||||
</a>
|
||||
<span className="shrink-0 whitespace-nowrap">
|
||||
· v{submission.revision} · {formatRelativeTime(submission.created)}
|
||||
@@ -405,9 +419,14 @@ const SubmissionCard = ({ submission, backend, userVote, onVote }: SubmissionCar
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="min-h-[4.5rem] flex-1 overflow-hidden">
|
||||
{submission.outdated && (
|
||||
{(submission.outdated || submission.reportCount > 0) && (
|
||||
<div className="mb-2 flex flex-wrap gap-1.5">
|
||||
<Badge variant="secondary">Outdated</Badge>
|
||||
{submission.outdated && <Badge variant="secondary">Outdated</Badge>}
|
||||
{submission.reportCount > 0 && (
|
||||
<Badge variant="destructive" className="gap-1">
|
||||
<IconFlag size={12} /> {submission.reportCount} {submission.reportCount === 1 ? "report" : "reports"}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{submission.description && (
|
||||
|
||||
@@ -162,7 +162,7 @@ export const useWorkshop = (workshopUrl: string) => {
|
||||
const report = useCallback(
|
||||
async (
|
||||
submissionId: string,
|
||||
category: "malicious" | "spam" | "copyright" | "inappropriate" | "other",
|
||||
category: "outdated" | "malicious" | "spam" | "copyright" | "inappropriate" | "other",
|
||||
explanation: string,
|
||||
) => {
|
||||
try {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap");
|
||||
@import "https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Work+Sans:wght@400;500;600;700&display=swap";
|
||||
@import "tailwindcss/theme" layer(theme);
|
||||
@import "tailwindcss/utilities" layer(utilities);
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
--breakpoint-3xl: 1697px;
|
||||
|
||||
--font-heading: var(--font-sans);
|
||||
--font-sans: "Inter", sans-serif;
|
||||
--font-sans: "Work Sans", sans-serif;
|
||||
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
@@ -137,7 +137,7 @@
|
||||
--sidebar-border: oklch(0.9276 0.0058 264.5313);
|
||||
--sidebar-ring: oklch(0.56 0.205 25.6509);
|
||||
--destructive-foreground: oklch(0.9851 0 0);
|
||||
--font-sans: "Inter", sans-serif;
|
||||
--font-sans: "Work Sans", sans-serif;
|
||||
--font-serif: Georgia, serif;
|
||||
--font-mono: "JetBrains Mono", monospace;
|
||||
--shadow-color: #000000;
|
||||
@@ -491,6 +491,10 @@ html[data-theme="dark"] div#__docusaurus div[role="banner"] button svg {
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.hover-animation {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.submit-step-enter {
|
||||
animation: none;
|
||||
}
|
||||
@@ -541,7 +545,7 @@ html[data-theme="dark"] div#__docusaurus div[role="banner"] button svg {
|
||||
--sidebar-accent-foreground: oklch(0.6646 0.2222 25.6509);
|
||||
--sidebar-border: oklch(0.2739 0.0055 286.0326);
|
||||
--sidebar-ring: oklch(0.72 0.16 25.6509);
|
||||
--font-sans: "Inter", sans-serif;
|
||||
--font-sans: "Work Sans", sans-serif;
|
||||
--font-serif: Georgia, serif;
|
||||
--font-mono: "JetBrains Mono", monospace;
|
||||
--shadow-color: #000000;
|
||||
|
||||
@@ -28,13 +28,260 @@ section {
|
||||
}
|
||||
|
||||
.heroBanner {
|
||||
padding: 4rem;
|
||||
padding: clamp(3rem, 6vw, 6rem) clamp(1.25rem, 4vw, 4rem);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 996px) {
|
||||
.heroBanner {
|
||||
padding: 2rem;
|
||||
.heroContent {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(25rem, 1fr) minmax(44rem, 48rem);
|
||||
align-items: center;
|
||||
gap: clamp(1rem, 3vw, 3rem);
|
||||
max-width: 108rem;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.heroCopy {
|
||||
max-width: 52rem;
|
||||
}
|
||||
|
||||
.heroTitle {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.heroActions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.heroPreview {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.heroCards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem 0.75rem;
|
||||
width: min(100%, 48rem);
|
||||
margin-inline: auto;
|
||||
color: #343a40;
|
||||
}
|
||||
|
||||
:global([data-theme="dark"]) .heroCards {
|
||||
color: #dee2e6;
|
||||
}
|
||||
|
||||
.widgetCard {
|
||||
flex: none;
|
||||
min-width: 0;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid color-mix(in srgb, currentcolor 12%, transparent) !important;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
background: color-mix(in srgb, var(--ifm-navbar-background-color) 76%, transparent) !important;
|
||||
backdrop-filter: blur(12px);
|
||||
box-shadow: 0 12px 28px rgb(31 41 55 / 14%);
|
||||
}
|
||||
|
||||
.resourcesWidget,
|
||||
.dockerWidget {
|
||||
align-items: stretch !important;
|
||||
justify-content: flex-start !important;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem !important;
|
||||
}
|
||||
|
||||
.widgetHeading,
|
||||
.resourceLabel,
|
||||
.containerRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.widgetTitle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
min-width: 0;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.onlineStatus,
|
||||
.dockerCount {
|
||||
flex: none;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.onlineStatus {
|
||||
color: #087f5b;
|
||||
}
|
||||
|
||||
:global([data-theme="dark"]) .onlineStatus {
|
||||
color: #69dbb1;
|
||||
}
|
||||
|
||||
.resourceMeter {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.resourceMeterCritical .resourceLabel {
|
||||
color: #a51111;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.resourceLabel {
|
||||
font-size: 0.6875rem;
|
||||
}
|
||||
|
||||
.resourceLabel > span:first-child {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.resourceTrack {
|
||||
display: block;
|
||||
appearance: none;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 0.375rem;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, currentcolor 16%, transparent);
|
||||
}
|
||||
|
||||
.resourceTrack::-webkit-progress-bar {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.resourceTrack::-webkit-progress-value,
|
||||
.resourceTrack::-moz-progress-bar {
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: var(--ifm-color-primary);
|
||||
}
|
||||
|
||||
.resourceMeterCritical .resourceTrack::-webkit-progress-value,
|
||||
.resourceMeterCritical .resourceTrack::-moz-progress-bar {
|
||||
background: #e03131;
|
||||
}
|
||||
|
||||
.downloadProgress {
|
||||
display: block;
|
||||
appearance: none;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 0.5rem;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, currentcolor 18%, transparent);
|
||||
}
|
||||
|
||||
.downloadProgress::-webkit-progress-bar {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.downloadProgress::-webkit-progress-value,
|
||||
.downloadProgress::-moz-progress-bar {
|
||||
border-radius: inherit;
|
||||
background: #2f9e44;
|
||||
}
|
||||
|
||||
.containerList {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.containerRow {
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.containerName {
|
||||
overflow: hidden;
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.containerStatus {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
color: color-mix(in srgb, currentcolor 82%, transparent);
|
||||
}
|
||||
|
||||
.statusDot {
|
||||
width: 0.375rem;
|
||||
height: 0.375rem;
|
||||
border-radius: 999px;
|
||||
background: #12b886;
|
||||
}
|
||||
|
||||
.containerRowFailed {
|
||||
color: #a51111;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
:global([data-theme="dark"]) .resourceMeterCritical .resourceLabel,
|
||||
:global([data-theme="dark"]) .containerRowFailed {
|
||||
color: #ffe3e3;
|
||||
}
|
||||
|
||||
.statusDotFailed {
|
||||
background: #fa5252;
|
||||
animation: failed-container-pulse 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes failed-container-pulse {
|
||||
50% {
|
||||
opacity: 0.35;
|
||||
transform: scale(1.65);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1280px) {
|
||||
.heroBanner {
|
||||
padding: 3rem 1.5rem;
|
||||
}
|
||||
|
||||
.heroContent {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 2.5rem;
|
||||
}
|
||||
|
||||
.heroCopy {
|
||||
max-width: 46rem;
|
||||
}
|
||||
|
||||
.heroCards {
|
||||
width: min(100%, 48rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 996px) {
|
||||
.heroBanner {
|
||||
padding: 2.5rem 1rem;
|
||||
}
|
||||
|
||||
.heroPreview {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.heroActions > :global(*) {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.statusDotFailed {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export function getRndInteger(min: number, max: number) {
|
||||
return Math.round(Math.random() * (max - min)) + min;
|
||||
}
|
||||
+11
-2
@@ -1266,9 +1266,15 @@
|
||||
"properties": {
|
||||
"boardId": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"layoutId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["boardId"],
|
||||
"required": ["boardId", "name", "layoutId"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
@@ -2151,7 +2157,9 @@
|
||||
"beszelAlerts",
|
||||
"beszelSystemStats",
|
||||
"traefik",
|
||||
"customApi"
|
||||
"customApi",
|
||||
"assistant",
|
||||
"wud"
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
@@ -2164,6 +2172,7 @@
|
||||
},
|
||||
"integrationIds": {
|
||||
"default": [],
|
||||
"maxItems": 32,
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 141 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
Loaded 100 of 1067 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user