Compare commits

..
Author SHA1 Message Date
ajnart bfd0bb7f80 test: remove obsolete advanced interactions e2e 2026-08-14 15:14:36 +02:00
ajnart 2098f89584 test: remove obsolete advanced focus assertion 2026-08-14 14:47:03 +02:00
2106 changed files with 74999 additions and 156994 deletions

No files matched your search

-40
View File
@@ -1,40 +0,0 @@
---
name: codebase-context
description: Navigate Homarr's monorepo architecture and reuse shared packages. Use when locating code, choosing a package boundary, understanding runtime services, tRPC, databases, widgets, integrations, cron jobs, Custom Widgets, Workshop, or checking @homarr/common before creating a utility.
---
# Homarr Codebase Context
Orient from the current checkout before editing. Treat package manifests, exports, and source as authoritative when they differ from this snapshot.
## Workflow
1. Confirm the branch and inspect the nearest `package.json`, existing sibling modules, and package exports.
2. Search `@homarr/common` before creating a general-purpose helper. Read [references/common.md](references/common.md) for every public entrypoint and export.
3. Read [references/architecture.md](references/architecture.md) when the task crosses packages, runtimes, routing, data, widgets, integrations, cron jobs, Custom Widgets, or Workshop.
4. Keep the change in the deepest package that owns the behavior. Reuse public entrypoints rather than deep-importing implementation files.
5. Re-run targeted searches before documenting counts, consumers, or dependency edges; these change faster than the architectural seams.
## Current toolchain
- Use Node `24.18.0` from `mise.toml` and pnpm `11.15.1` from `package.json`.
- Use pnpm workspaces with Turborepo. Workspace packages use the `@homarr/` scope and catalog-managed dependencies.
- Use Next.js App Router, TypeScript, tRPC, Drizzle, Mantine v9, Tabler icons, Jotai, TanStack Query, next-intl, Vitest, and Playwright.
- Use oxlint and oxfmt. Mantine is the application UI system; Tailwind is limited to the docs app.
- `pnpm dev` starts only `@homarr/nextjs`. Start a standalone runtime explicitly when the task needs it.
## High-value seams
- Server/RSC tRPC: `@homarr/api/server`
- Client tRPC hooks: `@homarr/api/client`
- Main router: `packages/api/src/root.ts`
- MCP eager router: `packages/api/src/mcp.ts`
- Database schemas: `packages/db/schema/{sqlite,mysql,postgresql}.ts`
- Widget registry: `packages/widgets/src/registry.ts`
- Widget loading manifest: `packages/widgets/src/manifest.ts`
- Integration definitions: `packages/definitions/src/integration.ts`
- Integration factory: `packages/integrations/src/base/creator.ts`
- Cron jobs: `packages/cron-jobs/src/jobs/`
- App routes: `apps/nextjs/src/app/[locale]/`
Invoke `documentation-sync` when hidden capabilities or information not evident to an advanced user change, or existing guidance becomes inaccurate; follow the Documentation Sync criteria in `AGENTS.md`. When exposing tRPC through MCP, invoke `mcp-integration`.
@@ -1,4 +0,0 @@
interface:
display_name: "Homarr Codebase Context"
short_description: "Navigate Homarr architecture and shared APIs"
default_prompt: "Use $codebase-context to locate the right Homarr package and reuse existing shared APIs."
@@ -1,110 +0,0 @@
# Homarr architecture
## Contents
- [Applications and runtimes](#applications-and-runtimes)
- [Package map](#package-map)
- [Dependency direction](#dependency-direction)
- [Core patterns](#core-patterns)
- [Commands and conventions](#commands-and-conventions)
## Applications and runtimes
| Path | Role |
| ---------------- | ---------------------------------------------------------------------------------------- |
| `apps/nextjs` | Main Next.js application and HTTP APIs on port 3000 |
| `apps/docs` | Docusaurus documentation site on port 3003 |
| `apps/tasks` | Cron-job initialization and scheduling runtime; it does not expose the old port-3002 API |
| `apps/websocket` | tRPC WebSocket subscriptions on port 3001 |
| `apps/workshop` | Go/PocketBase Workshop service and provider integration |
`pnpm dev` runs only `@homarr/nextjs`. Use `pnpm --filter @homarr/tasks dev`, `pnpm --filter @homarr/websocket dev`, or `pnpm dev:docs` when a task needs those runtimes.
In production, `apps/nextjs/src/instrumentation-node.ts` starts the tasks and WebSocket runtimes inside the Node process. Nginx listens on port 7575, proxies `/websockets` to port 3001, and sends other traffic to port 3000. Redis can run internally or externally.
## Package map
### Foundation and domain
- `core`: environment validation, database and Redis factories, logging, infrastructure.
- `definitions`: domain enums, integration definitions, widget kinds, permissions.
- `common`: shared utilities with environment-specific entrypoints. Search it before adding helpers.
- `validation`: shared Zod schemas.
- `server-settings`: typed server-setting keys and access.
### Data, identity, and transport
- `db`: Drizzle schemas, queries, migrations, and three database drivers.
- `auth`: Auth.js/NextAuth configuration, sessions, providers, and API keys.
- `api`: tRPC routers, contexts, MCP metadata extraction, OpenAPI, and client/server entrypoints.
- `redis`: Redis clients, pub/sub channels, and caches.
- `cron-jobs-core`, `cron-jobs`, `cron-job-status`: scheduling primitives, job implementations, and status transport.
### Backends and external systems
- `integrations`: typed clients for external applications.
- `request-handler`: shared server request flows.
- `docker`, `ping`, `image-proxy`, `icons`, `analytics`: bounded infrastructure capabilities.
- `custom-widgets`: Custom JSX v2 schema, validation, runtime, and security contracts.
- `workshop`: TypeScript Workshop client, backend contracts, and schemas used by Homarr.
### UI and composed features
- `ui`: shared Mantine components, theme, and hooks. Search here before creating a primitive.
- `form`, `forms-collection`: Zod-backed form setup and reusable forms.
- `modals`, `modals-collection`: modal primitives and feature modals.
- `boards`: board state, edit mode, and cache coordination.
- `settings`, `spotlight`, `notifications`, `onboarding`: cross-feature UI flows.
- `widgets`: widget definitions, dynamic component loaders, and shared widget UI.
- `translation`: next-intl configuration and locale catalogs.
- `cli`: Node operations CLI. `tools/homarr-dev` is the separate Go developer CLI.
## Dependency direction
Prefer these directions; verify the exact dependency in package manifests before changing a boundary.
1. Put infrastructure in `core` and stable domain names in `definitions`.
2. Put reusable, dependency-light helpers in `common`; use its public environment-specific entrypoints.
3. Build persistence in `db`, identity in `auth`, transport in `redis`, and external clients in `integrations`.
4. Compose server behavior in `api`, `cron-jobs`, and request-handler packages.
5. Build primitives in `ui`, `form`, and `modals`; compose product surfaces in widgets and collection packages.
6. Keep app entrypoints in `apps/*` thin. Avoid moving reusable behavior into `apps/nextjs` when a package already owns it.
## Core patterns
### tRPC and MCP
`packages/api/src/root.ts` lazily composes the application router. Server components use `@homarr/api/server`; client components use `@homarr/api/client`. MCP extraction requires the eager router in `packages/api/src/mcp.ts`, so MCP-enabled subrouters must be imported and registered there directly. Use the `mcp-integration` skill for exposure and security rules.
### Databases
Drizzle schemas live in parallel files under `packages/db/schema/`. `DB_DRIVER` selects `better-sqlite3`, `mysql2`, or `node-postgres`; migrations live in matching directories under `packages/db/migrations/`. Apply schema behavior consistently across all supported drivers unless the task is explicitly driver-specific.
### Widgets
`packages/widgets/src/registry.ts` owns the explicit widget-module and component-loader registries. `packages/widgets/src/manifest.ts` caches those loaders and exposes definitions, components, and combined resources. Widget modules use `createWidgetDefinition` and typed option builders. Data commonly comes from tRPC queries or Redis-backed cron results. Use existing shared widget components before creating local variants.
### Integrations
`packages/definitions/src/integration.ts` owns `integrationDefs` and `IntegrationKind`. `packages/integrations/src/base/creator.ts` owns `createIntegrationAsync`; concrete integrations implement the shared base contract. Keep credentials and permission checks on server-owned paths.
### Cron jobs
Jobs live in `packages/cron-jobs/src/jobs/` and use `createCronJob`. `apps/tasks` initializes and starts the job group. Job results and status can flow through Redis and tRPC; the retired port-3002 management-API model is no longer part of the architecture.
### Routing and localization
User routes live below `apps/nextjs/src/app/[locale]/`; the locale segment does not add a URL prefix. Route groups organize home and board surfaces. Administrative routes live under `manage`, Custom Widget configuration has a token route, and Workshop has a management surface.
### Custom Widgets and Workshop
`packages/custom-widgets` owns the Custom JSX v2 contract. `packages/workshop` is the Homarr-side client/contract package; `apps/workshop` is the separate Go/PocketBase service. Use the `homarr-custom-widget` skill for authoring and security constraints.
## Commands and conventions
- Read current scripts from the root or package `package.json`; avoid caching commands that do not exist.
- Use `pnpm dev` for Next.js, `pnpm dev:docs` for docs, and `pnpm dev:cli -- <args>` for the Go developer CLI.
- Use `pnpm docker:dev:up` for the Redis development dependency.
- Use package-filtered typechecks or focused tests for the touched behavior.
- Use `~/*` only inside the Next.js app. Use public `@homarr/*` entrypoints across package boundaries.
- Keep Mantine application styles separate from the Tailwind-based docs app.
@@ -1,85 +0,0 @@
# `@homarr/common` public API
## Contents
- [Reuse workflow](#reuse-workflow)
- [Entrypoints](#entrypoints)
- [Root exports](#root-exports)
- [Types exports](#types-exports)
- [Server exports](#server-exports)
- [Client and environment exports](#client-and-environment-exports)
## Reuse workflow
Before writing a utility, search both the public name and the behavior:
```bash
rg -n "export (const|function|class|type|interface)|export \*" packages/common
rg -n "@homarr/common" apps packages
```
Import only from an exported package entrypoint. Do not deep-import `@homarr/common/src/*`. Check `packages/common/package.json` and the source again when adding or renaming an export; this inventory describes the current repository tree.
## Entrypoints
| Import | Source | Environment |
| ----------------------- | ---------------------------- | ----------------------------------------------------------------------------------- |
| `@homarr/common` | `index.ts` -> `src/index.ts` | Shared root; includes React hooks and some Next/Mantine-aware helpers |
| `@homarr/common/types` | `src/types.ts` | Type-only utilities |
| `@homarr/common/server` | `src/server.ts` | Node/server-only security, encryption, request, error, and bounded-response helpers |
| `@homarr/common/client` | `src/client.ts` | Client-facing Next server action export |
| `@homarr/common/env` | `env.ts` | Validated package environment |
## Root exports
| Area | Public exports | Purpose |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| Objects | `objectKeys`, `objectEntries`, `isRecord`, `hashObjectBase64` | Typed key/entry iteration, record guards, and stable query-key hashing |
| Strings | `capitalize`, `isNullOrWhitespace`, `bestMatch`, `normalizeImageName`, `getImageMatchRank` | Text normalization and matching |
| Cookies | `parseCookies`, `setClientCookie` | Cookie parsing and browser cookie serialization |
| Arrays | `splitToNChunks`, `splitToChunksWithNItems` | Split by number of groups or maximum group size |
| Dates | `formatDuration`, `toValidDate`, `isDateWithin` | Duration formatting, safe date coercion, relative-window checks |
| Timing | `Stopwatch` | Elapsed-time measurement and formatting |
| React hooks | `useTimeAgo`, `useIntegrationConnected` | Relative time and integration freshness state |
| URLs | `removeTrailingSlash`, `extractBaseUrlFromHeaders`, `getPortFromUrl`, `isAbsoluteUrl`, `SAFE_NEW_TAB_REL`, `getSafeApplicationUrl`, `getSafeAppHref` | URL normalization, safe navigation, ports, and new-tab policy |
| Numbers and bytes | `formatNumber`, `formatBytes`, `formatBytesPair`, `formatByteRate`, `formatBitRate`, `defaultByteUnitSystem`, `metricToImperial` | Compact numbers, byte displays, byte/bit rates, and unit conversion |
| Byte types | `ByteUnitSystem`, `FormatBytesOptions`, `FormatBitRateOptions` | Options for byte and bit-rate formatting |
| Errors | `extractErrorMessage`, `FlattenError` | Unknown-error messages and flattenable domain errors |
| Theme | `getMantineColor` | Resolve a Mantine color and shade |
| Functions | `isFunction` | Unknown-value function guard |
| Grid | `GridAlgorithmItem`, `generateResponsiveGridFor` | Responsive nested-section grid placement |
| IDs | `createId` | CUID2 ID creation |
The root deliberately does not re-export `types`, `server`, `client`, or `env`; use their named entrypoints.
## Types exports
`@homarr/common/types` exports:
- `MaybePromise<T>`
- `AtLeastOneOf<T>`
- `Modify<T, R>`
- `RemoveReadonly<T>`
- `MaybeArray<T>`
- `Inverse<T>`
- `inferSearchParamsFromSchema<TSchema>`
## Server exports
`@homarr/common/server` exports:
- Security: `generateSecureRandomToken`.
- Encryption: `encryptSecret`, `decryptSecret`, `decryptSecretWithKey`, `createKeyedFingerprint`, `verifyKeyedFingerprint`.
- Requests: `userAgent`, `DeviceType`, `ipAddressFromHeaders`.
- Bounded bodies: `readBoundedJsonResponseAsync`.
- Parse errors: `ParseError`, `ParseErrorHandler`, `ZodParseErrorHandler`, `JsonParseErrorHandler`.
- HTTP handlers: `HttpErrorHandler`, `FetchHttpErrorHandler`, `matchErrorCode`, `OFetchHttpErrorHandler`, `AxiosHttpErrorHandler`, `TsdavHttpErrorHandler`, `OctokitHttpErrorHandler`.
- HTTP errors: `RequestError`, `ResponseError`, `requestErrorMap`.
- HTTP error types: `AnyRequestError`, `AnyRequestErrorInput`, `RequestErrorInput`, `RequestErrorType`, `RequestErrorReason`, `AnyRequestErrorReason`, `RequestErrorCode`.
`NodeFetchHttpErrorHandler` exists as an implementation file but is not exported by the public server entrypoint. Add an intentional public export before using it across packages.
## Client and environment exports
- `@homarr/common/client` exports `revalidatePathActionAsync`, a Next server action for page revalidation.
- `@homarr/common/env` exports `env` with validated `NODE_ENV`, `SECRET_ENCRYPTION_KEY`, and `NO_EXTERNAL_CONNECTION` values.
@@ -1,55 +0,0 @@
---
name: documentation-sync
description: Write concise Homarr docs for advanced users, covering only hidden capabilities and information not evident from the UI. Use when that information changes or existing guidance becomes inaccurate; skip obvious controls and routine fixes.
---
# Documentation Sync
Apply the audience and scope rules in `AGENTS.md` first. Before adding a passage, identify what an advanced user could not learn directly from the interface. If there is no such information, omit it.
For hidden capabilities such as the advanced widget feature, explain discovery, activation, and non-obvious behavior. For visible standard controls such as Delete, omit narration of what the label already conveys. Use the shortest explanation that preserves necessary technical detail; remove redundant prose in the passage being edited.
When an update is warranted, inspect adjacent pages for structure and types, not as a verbosity target. The mappings below locate needed information; they do not require a page for every feature or code change.
## Locate a needed update
| Code change | Documentation target |
| ----------------------------------- | -------------------------------------------------------------------- |
| New integration | `apps/docs/docs/integrations/<slug>/index.mdx` and `index.ts` |
| New widget | `apps/docs/docs/widgets/<slug>/index.mdx` and `index.ts` |
| Changed public or management API | `apps/docs/docs/management/api/index.mdx` |
| New or changed environment variable | Relevant page under `apps/docs/docs/advanced/` or installation docs |
| New or changed CLI command | `apps/docs/docs/advanced/command-line/` |
| Authentication/provider change | Relevant SSO or authentication page under `apps/docs/docs/advanced/` |
| UI or workflow change | Relevant getting-started, management, widget, or integration page |
| Cron-job behavior | `apps/docs/docs/management/tasks.mdx` |
| Permission behavior | `apps/docs/docs/management/users.mdx` and the affected feature page |
| Custom Widget or Workshop behavior | Relevant Custom Widget, Assistant, or Workshop documentation |
If no existing row fits, search by the user-visible term and update the page where a user would look for the behavior:
```bash
rg -n "<term>" apps/docs/docs apps/docs/src
```
## Follow existing typed patterns
For an integration:
- Add or update `apps/docs/docs/integrations/<slug>/index.ts` with the local `IntegrationDefinition` pattern.
- Add or update `index.mdx` with the established `IntegrationHeader`, `IntegrationCapabilites`, and `IntegrationSecrets` components as applicable.
- Reuse metadata from `@homarr/definitions` when the docs app already exposes it.
For a widget:
- Add or update `apps/docs/docs/widgets/<slug>/index.ts` with the local `WidgetDefinition` pattern.
- Add or update `index.mdx` with the established `WidgetHeader`, `WidgetConfig`, and `AddingWidget` components as applicable.
- Explain only non-obvious option semantics, integration prerequisites, permission constraints, or behavior that the interface does not make clear.
## Completion criteria
1. Every passage adds information an advanced user cannot reasonably infer from the interface. Remove obvious, repetitive, or unnecessary prose.
2. Keep names, defaults, paths, screenshots, links, and prerequisites consistent with code.
3. Update every affected page and remove superseded guidance.
4. Run the narrowest useful docs validation. Use `pnpm turbo build --filter=@homarr/docs` when links, MDX, generated definitions, or navigation can fail; otherwise run the docs package formatter on touched files.
5. Treat broken links and anchors as failures; Docusaurus checks them strictly.
@@ -1,4 +0,0 @@
interface:
display_name: "Homarr Documentation Sync"
short_description: "Keep user-facing docs aligned with code"
default_prompt: "Use $documentation-sync to map this Homarr code change to the required documentation updates."
+18 -21
View File
@@ -1,39 +1,36 @@
---
name: homarr-custom-widget
description: Author, validate, preview, install, or configure Homarr Custom JSX v2/v3 widgets, including static and native integration widgets.
description: Author safe Homarr Custom JSX v2 widgets from API documentation.
---
# Homarr Custom Widget
Load release-matched context. Run lifecycle tools alone; independent reads may run together. For a set, research once, then validate, preview, and persist each widget before the next.
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.
1. For HTTP widgets, read primary API documentation. For native capabilities, discover installed schemas. Static widgets need no service.
2. Create credential-free definitions with keyed `sources`, `requests`, optional `options`, and safe JSX `template`.
3. While drafting, use `customWidget_validateTemplate` for focused JSX diagnostics without resending the manifest.
4. Send the definition once to `customWidget_previewCreate` and test its HTTP/native queries and simulated actions. For a JSX-only fix, validate, call `customWidget_previewReviseTemplate` with its session, and retest; it inherits the manifest and resets evidence. Other definition changes require a fresh preview.
5. Configure deployment-specific source URLs and credentials through Homarr; never repeat plaintext.
6. Persist each exact final tested preview with `customWidget_createFromPreview`. Do not resend a large definition through `customWidget_create` when a preview session is available.
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.
Treat sample and preview responses as binding contracts. Render requested fields, guard optional values, humanize enums, and label timezones. Give errors and empty states a retry path.
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. 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.
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. Render load queries from `data` and `status` with `RefreshButton`; reserve `SubFetch` for manual parameterized queries. Templates read `data`, `status`, `options`, and temporary `inputs`.
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.
For a bound control that depends on another input, declare its default and set `resetKey` to that scalar dependency. For example, `<Pagination bind="page" defaultValue={1} resetKey={inputs.search} />` restores page 1 when a search changes without fetching by itself.
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`.
`SubFetch` with `trigger="manual"` renders its own load button. Pass a card or image through `triggerContent` with `triggerAriaLabel` when that content should launch the request. Its callback is `(result, meta)`; callback names must not shadow the reserved roots `data`, `status`, `options`, or `inputs`. Never author `onClick` or a fetch callback. Use `Icon` or `TablerIcon` with a `name`; never invent an `IconFoo` component.
`SubFetch` with `trigger="manual"` renders its own load button. Pass a card or image through `triggerContent` with `triggerAriaLabel` when that content should launch the request. Its callback is `(result, meta)`; never author `onClick` or a fetch callback.
Plan capabilities. Use one `customWidget_findComponents` search per job. Batch selected non-obvious binding or interaction documentation with `customWidget_getComponents`; reserve `customWidget_getComponent` for one unknown-prop repair. Failed validation reopens discovery; otherwise search only for a missing capability. The full catalog is for broad exploration; load at most one example. Context never limits composition. Prefer clear hierarchy, responsive grids, divided lists, and aligned actions over nested row cards.
Use standard Mantine compound names and deliberate visual hierarchy, spacing, restrained semantic color, and responsive layouts. Avoid excessive nested cards.
Compose installed components freely around the requested workflow. Use responsive layouts, clear hierarchy, quiet metadata, and actionable initial states. Wrap variable labels on narrow tiles and label interactive icons.
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.
For a community widget, use `customWidget_workshopSearch`, `customWidget_workshopGet`, then `customWidget_workshopInstall`; configure its source securely. Preview configuration expires, so create before persisting configuration.
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.
Reference routing:
Preview-scoped configuration expires with the preview. To persist user-entered values for a new widget, create the widget first, request configuration for its definition ID, then create a final preview with that definition ID so Homarr loads the stored credentials.
- Load `schema` once before a new manifest; otherwise only for concrete request or option ambiguity.
- Load `runtime` for bound inputs, manual queries, actions, or other interaction.
- Load `security` once for any authenticated source or mutation, or to resolve a URL or interpreter limitation.
Repository installations expose these as files under `references/`. MCP clients can call `customWidget_getReference` or read `homarr://custom-widgets/references/{name}` instead of loading every reference.
When this skill is installed from the repository, optional offline details are in `references/schema.md`, `references/runtime.md`, and `references/security.md`. MCP-only clients should use the live Homarr resources instead.
@@ -1,4 +0,0 @@
interface:
display_name: "Homarr Custom Widget"
short_description: "Build and deploy safe Custom JSX widgets"
default_prompt: "Use $homarr-custom-widget to author, validate, preview, and configure a safe Custom JSX v2 widget."
@@ -1,39 +1,16 @@
# Runtime
Templates read `data.requestId`, `status.requestId`, `options.name`, and temporary `inputs.name`. Status is `{ loading, ok, status, statusText, error }`. Render load queries directly from `data` and `status` with `RefreshButton`; never wrap them in `SubFetch`.
Templates read `data.requestId`, `status.requestId`, `options.name`, and temporary `inputs.name`. Status contains `loading`, `ok`, `status`, `statusText`, and `error`.
`bind="search"` creates an in-memory input. It is never persisted. Supply invocation values only through `params`, for example:
```jsx
<TextInput bind="search" label="Search" />
<Pagination bind="page" resetKey={inputs.search} defaultValue={1} total={5} />
<SubFetch requestId="search" trigger="manual" params={{ query: inputs.search }}>
{(result) => <Stack>{(result.results ?? []).map(item => <Text key={item.id}>{item.name}</Text>)}</Stack>}
<SubFetch requestId="search" params={{ query: inputs.search }}>
{(items) => <Stack>{(items ?? []).map(item => <Text key={item.id}>{item.name}</Text>)}</Stack>}
</SubFetch>
```
Manual queries require `trigger: "manual"` on request and `SubFetch`. Use `triggerContent` and `triggerAriaLabel` for a custom launcher. `SubFetch` owns loading/error/retry; its child receives success plus `{ ok, status, statusText, loading: false }`.
`SubFetch` with `trigger="manual"` renders its own load button. To make a card or image launch the request, pass that node through `triggerContent` and provide `triggerAriaLabel`; Homarr supplies the click and keyboard behavior. Its optional second callback argument contains `{ ok, status, statusText, loading: false }`; loading and request failures are rendered by `SubFetch` before the callback runs. Do not author `onClick` or a fetch callback.
`SubFetch`, `ActionButton`, and `ToggleSwitch` need literal `requestId`; validation rejects missing/computed IDs.
Inside a successful manual result, `<RefreshButton requestId="search" label="Run again" />` reruns the same parameters.
When a manual SubFetch request ID, parameters, or effective definition changes, Homarr immediately hides its prior result and returns to the trigger. It cannot fetch the new parameters until the user triggers it again.
The `SubFetch` callback receives the entire JSON response exactly as previewed. If the response is `{ "results": [...] }`, render and map `result.results`; never map the envelope itself. Trace every rendered field from the preview response before persistence.
Format timestamps with safe static helpers; never use `new Date`. Never invent a formatter component. Use `Date.toLocaleString(value, "en-US", "UTC")` plus a visible `UTC` label. Also available: `Date.toISOString`, `Date.toLocaleDateString`, and `Date.toLocaleTimeString`.
For compact numeric enums, index a literal label array with a fallback:
```jsx
<Text>{["Unknown", "Pending", "Ready"][(item.status ?? 1) - 1] ?? "Unknown"}</Text>
```
Every stateful control must use `bind`, and its `inputs.<name>` value must feed a supported request/helper when it is meant to change remote data. For dependent pagination, declare `defaultValue={1}` and use `resetKey={inputs.search}` to restore page 1 when the query changes. If a control cannot affect the workflow through a binding, option, or runtime helper, render concise context instead of a dead control.
Callback parameters must not shadow the reserved roots `data`, `status`, `options`, or `inputs`. Use `<Icon name="refresh" />` or `<TablerIcon name="refresh" />`; never invent components such as `<IconFoo />`.
Use expression callbacks for supported collections and trusted slots. No callback blocks, IIFEs, authored recursion, or raw events. Regex is limited to safe string operations.
V3 adds scoped `className` styles, `View` fragments, `WidgetModal`, `WidgetDrawer`, and `AppEmbed`. Bound controls use `persist` for declared browser preferences or `content` for shared drafts saved with `ContentSaveButton`. `NativeQuery` and `NativeActionButton` invoke declared native IDs; actions retain preview simulation and integration permissions. Fetch component contracts before using these helpers.
Use expression callbacks for supported collection methods and trusted slots. Do not use callback blocks, IIFEs, authored recursion, or raw events. Bounded regex literals work only with safe string matching/replacement operations.
@@ -13,39 +13,10 @@ interface HomarrCustomWidgetV2 {
}
```
Sources are keyed by ID. Empty `sources` and `requests` support static widgets. Each HTTP request must reference an existing source; omitting its source selects `default`. Source properties are `name?`, `baseUrl`, `networkScope`, and `auth?`:
```json
{
"sources": {
"default": {
"name": "Service",
"baseUrl": "http://service.local:5055/api/v1",
"networkScope": "private",
"auth": { "type": "apiKeyHeader", "name": "X-Api-Key" }
}
},
"requests": {
"summary": { "path": "/summary" },
"search": { "trigger": "manual", "path": "/search", "query": { "q": { "$param": "query" } } },
"create": {
"kind": "action",
"method": "POST",
"path": "/items",
"body": { "id": { "$param": "id" } },
"confirmation": "Create this item?",
"invalidates": ["search"]
}
}
}
```
Auth is `none`, `bearer`, `basic`, `{ "type": "apiKeyHeader", "name": "X-Api-Key" }`, or `{ "type": "apiKeyQuery", "name": "api_key" }`. A request defaults to source `default`, kind `query`, method `GET`, trigger `load`, inherited auth, and view permission. Set `trigger: "manual"` for a parameterized query. An action defaults to manual and modify permission. DELETE uses full permission and confirmation. Do not use `load: false`.
Sources require `default`. A request defaults to the default source, query, GET, load, inherited auth, and view permission. An action defaults to manual and modify permission. DELETE uses full permission and confirmation.
Use stable real URLs for public APIs and clear suggested URLs for self-hosted services. Homarr collects the installer's server URL, network scope, and credentials as source setup; credentials remain outside the manifest.
Paths use `{option:name}` and `{param:name}`; query/body references use `{ "$option": "name" }` and `{ "$param": "name" }`. Constants stay primitive (`take: 10`); `$param` is only for manual helpers, never load queries. Names and types are inferred.
Paths use `{option:name}` and `{param:name}`. Query/body data uses `{ "$option": "name" }` and `{ "$param": "name" }`. Load queries cannot use params. Runtime parameter names and primitive values are inferred from references.
Every option has `label`, `control`, and `default`. Optional fields are `description`, `choices`, `choicesFrom`, `min`, `max`, `step`, `advanced`, and `group`.
V3 uses `$schema: "homarr-custom-widget-v3"` and optional `extensions`: `stylesheet`, `fragments`, `preferences`, `content`, and `native`. Read `customWidget_schema` for their exact shapes. Integration options use `control: "integration"`, `default: ""`, and optional `integrationKinds`; installations choose local integrations. Discover native input schemas through `customWidget_nativeCapabilities`. Editor layout is separate from the manifest.
@@ -4,4 +4,4 @@ All requests use Homarr's protected server executor. Source origin, network scop
The JSX interpreter blocks imports, hooks, refs, raw event callbacks, browser requests, eval, arbitrary functions, prototype access, unsafe URLs, global CSS escape, arbitrary portals, bigint, statement blocks, IIFEs, and recursion. Regex literals must be bounded and reject backreferences, lookbehind, nested quantifiers, excessive length, and unsupported flags.
Credentials stay local and are never exported. Installers confirm or replace suggested private and loopback URLs for their deployment. Source origins are fixed by configuration.
Credentials are stored separately and never exported or returned to an agent. A published self-hosted source URL is only a suggestion: installers must confirm or replace private and loopback URLs for their own Homarr deployment. Source origins cannot be controlled through widget options.
-98
View File
@@ -1,98 +0,0 @@
---
name: mcp-integration
description: Expose Homarr tRPC procedures safely through MCP. Use when adding or changing tRPC procedures under packages/api, adding MCP metadata, registering an eager MCP router, shaping tool inputs and descriptions, or reviewing MCP authorization, sensitive data, and destructive actions.
---
# MCP Integration
Expose a procedure only when an AI client should call it. Preserve the procedure's existing authorization and add MCP-specific safeguards for sensitive or destructive behavior.
## Assess exposure
Prefer:
- Data queries: list, search, get, health, stats, and summaries.
- User-requested actions with explicit permission checks and bounded inputs.
- Serializable results that do not reveal credentials or internal-only state.
Skip subscriptions, session/onboarding internals, file streams, blobs, credential values, and procedures whose authorization or audit behavior is unclear. Ask for a security review when a mutation is destructive, tenant boundaries are ambiguous, or returned data is sensitive.
## Add metadata
Place `.meta()` before `.input()` and `.query()` or `.mutation()`:
```typescript
getAll: protectedProcedure
.meta({
mcp: {
enabled: true,
description: "List resources the current user can access, including stable IDs used by resource_get",
},
})
.input(z.object({ limit: z.number().int().min(1).max(100).default(50) }))
.query(async ({ ctx, input }) => {
// Preserve normal procedure authorization.
});
```
No-input procedures can omit `.input()`; the MCP extractor accepts an empty object for them.
For mutations, state the effect and required permission in the description:
```typescript
remove: permissionRequiredProcedure
.meta({
mcp: {
enabled: true,
description: "Delete one resource by ID. Requires full access; get the ID from resource_all",
},
})
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
// Perform the permission-scoped action.
});
```
## Write discoverable descriptions
Include every item that applies:
- What the tool returns or changes.
- Supported integrations or services.
- Required IDs and the tool that returns them.
- Permission requirements and the meaning of permission fields.
- Important bounds, confirmation semantics, or irreversible effects.
Use precise domain language. A description such as `Get calendar events` is insufficient because it omits supported services, required IDs, and result scope.
## Shape inputs for clients
- Use a top-level `z.object({...})` for parameterized tools.
- Bound strings, arrays, numeric ranges, pagination, and payload sizes.
- Use defaults where a safe, unsurprising value exists.
- Prefer explicit fields over a top-level union or discriminated union.
- Keep identifiers stable and describe how to obtain them.
- Keep secrets out of inputs unless the procedure is explicitly a credential-configuration flow with server-owned encryption and non-return guarantees.
## Register the eager router
The application router in `packages/api/src/root.ts` is lazy. MCP tool extraction is synchronous, so add an eager import and registration in `packages/api/src/mcp.ts`:
```typescript
import { resourceRouter } from "./router/resource";
export const mcpRouter = createTRPCRouter({
resource: resourceRouter,
});
```
Register the smallest router that owns the enabled procedures. If a parent router uses `lazy()`, import the needed subrouter directly. Metadata on a procedure is not enough; a procedure absent from `mcpRouter` is not exposed.
## Validate the tool surface
1. Confirm the procedure still enforces its ordinary auth and permission boundary.
2. Confirm `packages/api/src/mcp.ts` eagerly includes the procedure.
3. Confirm the generated tool name, description, and JSON schema are unambiguous.
4. Run the focused API MCP spec when the tool list or extraction changes: `pnpm test packages/api/src/test/mcp.spec.ts`.
5. Run the focused route spec when protocol or transport behavior changes: `pnpm test apps/nextjs/src/app/api/mcp/[transport]/route.spec.ts`.
6. If manually probing `/api/mcp/<transport>`, use a scoped API key and avoid printing credentials or secret-bearing results.
@@ -1,4 +0,0 @@
interface:
display_name: "Homarr MCP Integration"
short_description: "Expose tRPC procedures safely through MCP"
default_prompt: "Use $mcp-integration to expose this Homarr tRPC procedure as a safe, discoverable MCP tool."
-1
View File
@@ -1 +0,0 @@
../.agents/skills
+206
View File
@@ -0,0 +1,206 @@
---
description: Comprehensive Homarr codebase architecture and conventions reference
alwaysApply: true
---
# Homarr Codebase Context
Homarr is an open-source, self-hosted dashboard for managing homelab services. It integrates with 50+ self-hosted apps (media servers, download clients, DNS, NAS, etc.) and provides a drag-and-drop widget-based UI.
## Tech Stack
- **Monorepo**: pnpm workspaces + Turborepo
- **Framework**: Next.js (App Router, `output: "standalone"`) — T3 Stack
- **Language**: TypeScript throughout
- **ORM**: Drizzle (supports SQLite via better-sqlite3, MySQL, PostgreSQL)
- **API**: tRPC (HTTP + WebSocket subscriptions) with superjson + OpenAPI bridge via `trpc-to-openapi`
- **Auth**: NextAuth v5 (database sessions, Credentials/LDAP/OIDC providers)
- **UI**: Mantine v9 (not Tailwind) + Tabler icons
- **State**: Jotai (atoms), TanStack Query (server state via tRPC)
- **Realtime**: WebSocket server (`ws`) for tRPC subscriptions, backed by Redis pub/sub
- **Cron**: `node-cron` in a standalone Fastify service (`apps/tasks`)
- **i18n**: next-intl with `[locale]` dynamic segment (prefix mode: `"never"`, locale from cookie)
- **Testing**: Vitest + jsdom, Playwright for E2E
- **Lint/Format**: oxlint + oxfmt (not ESLint/Prettier)
- **Docs**: Docusaurus 3 in `apps/docs/` (`@homarr/docs`)
- **Package manager**: pnpm 10.34.1, Node >= 24.16.0
## Repository Structure
```
homarr/
├── apps/
│ ├── nextjs/ # Main Next.js application (port 3000)
│ ├── docs/ # Docusaurus 3 documentation site (@homarr/docs)
│ ├── tasks/ # Cron job runner + Fastify tRPC API (port 3002)
│ └── websocket/ # Standalone tRPC WebSocket server (port 3001)
├── packages/
│ ├── api/ # tRPC appRouter, procedures, OpenAPI
│ ├── auth/ # NextAuth config, providers, session, API keys
│ ├── db/ # Drizzle schema (3 DB drivers), migrations, queries
│ ├── core/ # Env validation, DB/Redis driver factories, logging
│ ├── definitions/ # Domain enums: WidgetKind, IntegrationKind, permissions
│ ├── widgets/ # All 39 dashboard widgets (definitions + components)
│ ├── integrations/ # Integration classes (HTTP clients to external apps)
│ ├── redis/ # Redis pub/sub channels, caching abstractions
│ ├── translation/ # next-intl setup, locale configs, lang JSON files
│ ├── ui/ # Shared Mantine components, theme, hooks
│ ├── validation/ # Shared zod schemas for API/forms
│ ├── common/ # Shared utilities, IDs, errors
│ ├── cron-jobs/ # Cron job implementations (25+ jobs)
│ ├── cron-jobs-core/ # Cron scheduling primitives
│ ├── cron-job-api/ # tRPC router for job management (start/stop/trigger)
│ ├── cron-job-status/ # Cron status via Redis
│ ├── boards/ # Board context, edit mode, cache updater
│ ├── modals/ # Modal primitives on Mantine
│ ├── modals-collection/ # Feature modals (apps, boards, docker, etc.)
│ ├── form/ # useZodForm (Mantine + zod resolver)
│ ├── forms-collection/# Reusable form UIs (new app, icon picker, upload)
│ ├── spotlight/ # Command palette / search with multiple modes
│ ├── request-handler/ # Server request handlers (feeds, integrations)
│ ├── notifications/ # Mantine notifications wrapper
│ ├── docker/ # Dockerode-based Docker access
│ ├── icons/ # Icon DB/repo integration
│ ├── image-proxy/ # Image proxy + caching
│ ├── ping/ # Reachability / ping utilities
│ ├── 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
│ └── github/ # CI setup action
├── development/ # Dev docker-compose (Redis, MySQL, PostgreSQL)
├── e2e/ # E2E test specs
└── Dockerfile # Multi-stage production build
```
## Three Runtime Services
| Service | Port | Role |
|---------|------|------|
| Next.js | 3000 | Main web app (SSR + API routes) |
| WebSocket | 3001 | tRPC subscriptions via `ws` |
| Tasks | 3002 | Cron job runner + management API via Fastify |
In Docker, nginx listens on **7575** and proxies to all three. The tasks service authenticates via `CRON_JOB_API_KEY` header.
## Package Dependency Layers
1. **Foundation**: `core` (env, DB drivers, Redis client, logging) → no `@homarr` deps
2. **Domain**: `definitions` → `common` → `core`
3. **Data**: `db` → `core`, `definitions`, `common`
4. **Auth**: `auth` → `db`, `definitions`, `core`, `validation`
5. **Cross-cutting**: `translation`, `validation`, `redis`, `server-settings`
6. **Feature backends**: `integrations`, `request-handler`, `cron-jobs`, `docker`, `ping`
7. **API surface**: `api` → pulls in auth, db, redis, integrations, etc.
8. **UI stack**: `ui`, `modals`, `form`, `notifications`, `boards`, `spotlight`
9. **Composed features**: `widgets`, `modals-collection`, `forms-collection`
## Key Patterns
### tRPC
- **Server caller** (RSC): `import { api } from "@homarr/api/server"` → cached caller with auth context
- **Client hooks**: `import { clientApi } from "@homarr/api/client"` → React Query hooks
- **Router structure**: `packages/api/src/root.ts` defines top-level namespaces (`user`, `board`, `widget`, `integration`, `docker`, `kubernetes`, etc.)
- **Procedures**: `publicProcedure`, `protectedProcedure`, `permissionRequiredProcedure`, `onboardingProcedure`
- **WebSocket subscriptions**: Client uses `wsLink` when `type === "subscription"`, connects to port 3001
### Multi-Database Support
Drizzle schemas exist in three parallel implementations:
- `packages/db/schema/sqlite.ts`
- `packages/db/schema/mysql.ts`
- `packages/db/schema/postgresql.ts`
Selected at runtime via `DB_DRIVER` env (`better-sqlite3` | `mysql2` | `node-postgres`). Migrations are per-engine in `packages/db/migrations/{sqlite,mysql,postgresql}/`.
### Widget System
Widgets are registered in `packages/widgets/src/index.tsx` as `widgetImports` (must satisfy `Record<WidgetKind, ...>`). Each widget:
- Has a `definition` created via `createWidgetDefinition(kind, { icon, createOptions, supportedIntegrations? })`
- Uses `optionsBuilder.from(factory => ({ ... }))` for typed options
- Loads dynamically via `next/dynamic`
- Fetches data via tRPC client hooks (backed by cron job caches)
### Integration System
Integrations are defined in `packages/definitions/src/integration.ts` (`integrationDefs`) and instantiated via `createIntegrationAsync` in `packages/integrations/src/base/creator.ts`. Each extends abstract `Integration` class with `testConnectionAsync` and typed methods.
### Cron Job System
Jobs defined in `packages/cron-jobs/src/jobs/`. Each job:
- Uses `createCronJob` with a cron expression
- Publishes results to Redis channels
- Widget subscriptions consume those channels via tRPC subscriptions
- Job management (start/stop/trigger) via `cronJobApi` HTTP client to port 3002
### Modals
Created with `createModal(component).withOptions({ defaultTitle })` from `@homarr/modals`. Opened via `useModalAction(SomeModal)`. Feature modals live in `packages/modals-collection/`.
### Forms
`useZodForm` from `@homarr/form` wraps Mantine `useForm` with zod validation. Shared form UIs in `packages/forms-collection/`.
### Routing (App Router)
- `apps/nextjs/src/app/[locale]/` — all UI routes under locale segment
- Route groups: `(home)`, `(content)`, `(board)` — don't affect URLs
- Key routes: `boards/[name]`, `manage/...` (admin), `auth/login`, `init` (onboarding), `widgets/[kind]`
- Locale driven by cookie (no URL prefix)
### Auth Providers
Configured via `AUTH_PROVIDERS` env. Supports: `credentials` (local), `ldap`, `oidc`. Database sessions with httpOnly cookies. API key auth for programmatic access.
## Environment Variables
Key env vars (from `.env.example`):
- `DB_DRIVER`: `better-sqlite3` | `mysql2` | `node-postgres`
- `DB_URL`: Database connection string
- `AUTH_SECRET`: NextAuth secret
- `SECRET_ENCRYPTION_KEY`: AES-256-CBC for integration secrets
- `AUTH_PROVIDERS`: Comma-separated (`credentials`, `ldap`, `oidc`)
- `CRON_JOB_API_KEY`: Shared key between Next.js and tasks service
- `REDIS_*`: Redis connection (host, port, password)
- `ENABLE_KUBERNETES`: Optional Kubernetes integration
## Development Commands
- `pnpm dev` — runs all three services in parallel via Turborepo
- `pnpm docker:dev` — starts Redis + MySQL + PostgreSQL containers
- `pnpm db:push` — push schema to SQLite (dev)
- `pnpm db:studio` — Drizzle Studio
- `pnpm test` — Vitest unit tests
- `pnpm test:e2e` — Playwright E2E
- `pnpm lint` / `pnpm format` — oxlint + oxfmt
- `pnpm dev:docs` — Docusaurus docs site (port 3003)
- `pnpm typecheck` — tsc --noEmit across all packages
## Conventions
- Workspace packages use `@homarr/` scope
- All versions managed via pnpm `catalog:` in workspace
- Path alias `~/*` → `./src/*` in Next.js app
- Mantine for all UI (no Tailwind) — primary color is red, `autoContrast: true`
- Icons from `@tabler/icons-react`
- Drag-and-drop via `@dnd-kit/*`
- `typescript.ignoreBuildErrors: true` in next.config (types checked separately via `typecheck`)
- `serverExternalPackages`: `dockerode`, `isomorphic-dompurify`, `jsdom`
## Documentation (`apps/docs`)
Docusaurus 3 site at `apps/docs/`, package name `@homarr/docs`.
- Run with `pnpm dev:docs` from root
- Build with `pnpm turbo build --filter=@homarr/docs`
- Uses Tailwind CSS 3 for custom pages (not Mantine)
- Imports `@homarr/definitions` for type-safe integration/widget metadata
- When adding or modifying widgets, integrations, cron jobs, API routes, env vars, or user-facing features, you MUST also update corresponding docs in `apps/docs/docs/`
- Integration docs: `apps/docs/docs/integrations/<slug>/index.mdx` + `index.ts`
- Widget docs: `apps/docs/docs/widgets/<slug>/index.mdx` + `index.ts`
- Strict link checking: broken links/anchors cause build failure
+50
View File
@@ -0,0 +1,50 @@
---
description: Keep documentation in sync with code changes
globs:
- packages/definitions/src/**
- packages/widgets/src/**
- packages/integrations/src/**
- packages/api/src/**
- apps/nextjs/src/**
- apps/docs/**
---
# Documentation Sync Rule
When modifying code that affects user-facing behavior, you MUST also update the documentation in `apps/docs/`.
## What triggers a doc update
| Code change | Doc location |
|---|---|
| New integration | `apps/docs/docs/integrations/<slug>/index.mdx` + `index.ts` |
| New widget | `apps/docs/docs/widgets/<slug>/index.mdx` + `index.ts` |
| Changed API surface | `apps/docs/docs/management/api.mdx` |
| New/changed env vars | `apps/docs/docs/advanced/` |
| New CLI commands | `apps/docs/docs/advanced/command-line/` |
| Changed auth providers | `apps/docs/docs/advanced/` SSO pages |
| UI/UX changes | Relevant getting-started or management pages |
| New cron job | `apps/docs/docs/management/tasks.mdx` |
| Changed permissions | `apps/docs/docs/management/users.mdx` |
## Integration doc pattern
Each integration has a folder under `apps/docs/docs/integrations/<slug>/`:
- `index.ts` — exports a typed `IntegrationDefinition` with name, description, iconUrl, path
- `index.mdx` — uses shared components: `IntegrationHeader`, `IntegrationCapabilites`, `IntegrationSecrets`
- Import metadata from `@homarr/definitions` where possible
## Widget doc pattern
Each widget has a folder under `apps/docs/docs/widgets/<slug>/`:
- `index.ts` — exports a typed `WidgetDefinition` with icon, name, description, path, configuration
- `index.mdx` — uses shared components: `WidgetHeader`, `WidgetConfig`, `WidgetAdding`
## Development
- Run docs locally: `pnpm dev:docs` from root
- Build docs: `pnpm turbo build --filter=@homarr/docs`
- The docs app can import from `@homarr/definitions` for type-safe integration/widget metadata
- Docusaurus strict mode throws on broken links — fix any broken references before committing
@@ -0,0 +1,622 @@
# Migrating Tables from mantine-react-table to mantine-datatable
This document captures every pattern, fix, and decision from the downloads widget rework (PR #6430). Use it as a playbook when porting other tables.
## Migration Candidates
Current `mantine-react-table` consumers that could be migrated:
**Widgets (in `packages/widgets/`):**
- `docker/component.tsx` — Docker container table
- `media-server/component.tsx` — Media server sessions
**Admin pages (in `apps/nextjs/src/app/[locale]/manage/`):**
- `tools/docker/docker-table.tsx`
- `tools/tasks/_components/tasks-table.tsx`
- `tools/api/components/api-keys.tsx`
- `users/_components/user-list.tsx`
- `users/invites/_components/invite-list.tsx`
- `tools/kubernetes/` — 8 Kubernetes resource tables
**Modals:**
- `packages/modals-collection/src/search-engines/request-media-modal.tsx`
**Shared hook to remove once all migrated:**
- `packages/ui/src/hooks/use-translated-mantine-react-table.ts`
---
## 1. Core Migration: mantine-react-table → mantine-datatable
### Import changes
```typescript
// BEFORE
import { MantineReactTable, useMantineReactTable } from "mantine-react-table";
import type { MRT_ColumnDef } from "mantine-react-table";
// AFTER
import { DataTable, useDataTableColumns } from "mantine-datatable";
import type { DataTableColumn, DataTableSortStatus } from "mantine-datatable";
```
### Column definition shape
```typescript
// BEFORE (MRT)
const columns: MRT_ColumnDef<MyRow>[] = [
{
accessorKey: "name",
header: "Name",
size: 200,
enableSorting: true,
Cell: ({ row }) => <Text>{row.original.name}</Text>,
},
];
// AFTER (mantine-datatable)
const columns: DataTableColumn<MyRow>[] = [
{
accessor: "name", // not accessorKey
title: "Name", // not header
width: 200, // not size
sortable: true, // not enableSorting
render: (record) => <Text>{record.name}</Text>, // not Cell
},
];
```
Key differences:
- `accessorKey` → `accessor`
- `header` → `title`
- `size` → `width`
- `Cell: ({ row }) =>` → `render: (record) =>`
- `enableSorting` → `sortable`
- `enableColumnActions` → does not exist (no column action menus)
- `mantineTableBodyCellProps` → `cellsStyle` in `defaultColumnProps`
### Table component
```typescript
// BEFORE
const table = useTranslatedMantineReactTable({ columns, data, ... });
return <MantineReactTable table={table} />;
// AFTER
return (
<DataTable
records={data}
columns={columns}
sortStatus={sortStatus}
onSortStatusChange={setSortStatus}
...
/>
);
```
### Sorting is manual
mantine-datatable does NOT sort for you. You must sort the data yourself:
```typescript
const [sortStatus, setSortStatus] = useState<DataTableSortStatus<MyRow>>({
columnAccessor: "name",
direction: "asc",
});
const sortedData = useMemo(() => {
const mult = sortStatus.direction === "desc" ? -1 : 1;
return [...data].toSorted((a, b) => {
const aVal = a[sortStatus.columnAccessor];
const bVal = b[sortStatus.columnAccessor];
if (typeof aVal === "string") return aVal.localeCompare(bVal as string) * mult;
return ((aVal as number) - (bVal as number)) * mult;
});
}, [data, sortStatus]);
// Pass sortedData to records, not data
<DataTable records={sortedData} sortStatus={sortStatus} onSortStatusChange={setSortStatus} />
```
---
## 2. Making Tables Transparent (Widget Context)
Widgets sit inside board cards that have user-controlled opacity. The table must be fully transparent so the card background shows through. mantine-datatable defaults to `var(--mantine-color-body)` which is opaque.
Create a CSS file alongside the component:
```css
/* styles.css */
.my-table,
.my-table .mantine-datatable-table,
.my-table .mantine-datatable-table thead,
.my-table .mantine-datatable-table tbody,
.my-table .mantine-datatable-table tfoot,
.my-table th,
.my-table td {
background-color: transparent !important;
}
.my-table .mantine-datatable-table tr {
background-color: transparent;
}
/* Hover: translucent instead of opaque */
.my-table .mantine-datatable-table tbody tr:hover {
background-color: color-mix(in srgb, var(--mantine-color-default-hover) 40%, transparent) !important;
}
/* Headers: frosted glass effect */
.my-table th {
white-space: nowrap;
background-color: color-mix(in srgb, var(--mantine-color-body) 60%, transparent) !important;
backdrop-filter: blur(8px);
}
/* Cell overflow */
.my-table td {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Hide sort icon until hover */
.my-table .mantine-datatable-header-cell-sortable-icon {
opacity: 0;
transition: opacity 150ms;
}
.my-table th:hover .mantine-datatable-header-cell-sortable-icon {
opacity: 1;
}
/* Row expansion area */
.my-table .mantine-datatable-row-expansion-cell {
background-color: color-mix(in srgb, var(--mantine-color-body) 40%, transparent) !important;
}
```
Import it: `import "./styles.css";` and apply via `className="my-table"`.
---
## 3. Responsive Column Visibility
Use a lookup table of width breakpoints instead of media queries. The widget receives `width` as a prop.
```typescript
interface SizeConfig {
fontSize: "xs" | "sm";
iconSize: number;
cellPadding: number;
showSpeedColumns: boolean;
showTimeColumn: boolean;
showStateColumn: boolean;
}
const SIZE_BREAKPOINTS: { maxWidth: number; config: SizeConfig }[] = [
{ maxWidth: 300, config: { fontSize: "xs", iconSize: 12, cellPadding: 2, showSpeedColumns: false, showTimeColumn: false, showStateColumn: false } },
{ maxWidth: 500, config: { fontSize: "xs", iconSize: 14, cellPadding: 4, showSpeedColumns: false, showTimeColumn: true, showStateColumn: false } },
];
const DEFAULT_SIZE_CONFIG: SizeConfig = { fontSize: "sm", iconSize: 14, cellPadding: 4, showSpeedColumns: true, showTimeColumn: true, showStateColumn: true };
function getSizeConfig(width: number): SizeConfig {
for (const { maxWidth, config } of SIZE_BREAKPOINTS) {
if (width < maxWidth) return config;
}
return DEFAULT_SIZE_CONFIG;
}
// Usage: memoize it
const size = useMemo(() => getSizeConfig(width), [width]);
```
Then use a visibility check lookup table for columns:
```typescript
const columnVisibilityChecks: Partial<Record<string, (ctx: ColumnContext) => boolean>> = {
upSpeed: (ctx) => ctx.hasTorrents && ctx.size.showSpeedColumns,
downSpeed: (ctx) => ctx.size.showSpeedColumns,
time: (ctx) => ctx.size.showTimeColumn,
state: (ctx) => ctx.size.showStateColumn,
};
function shouldIncludeColumn(accessor: string, ctx: ColumnContext): boolean {
if (!ctx.optionsColumnSet.has(accessor)) return false;
const check = columnVisibilityChecks[accessor];
if (check && !check(ctx)) return false;
return true;
}
```
---
## 4. Column Persistence (Drag/Resize → Server)
`useDataTableColumns` stores column order and widths in localStorage by default. For widgets, this must persist server-side so it works across devices.
### Hidden widget options
In `index.ts`, add hidden text options:
```typescript
columnOrder: factory.text({ defaultValue: "" }),
columnWidths: factory.text({ defaultValue: "" }),
```
With visibility override:
```typescript
{
columnOrder: { shouldHide: () => true },
columnWidths: { shouldHide: () => true },
}
```
### Translation keys required
Even hidden options need translation keys or the translation test fails:
```json
"widget.mywidget.option.columnOrder.label": "Column order",
"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:
```typescript
const storeKey = `mytable-${itemId ?? "preview"}-${[...options.columns].toSorted().join(",")}`;
```
### Hydration from server → localStorage
On mount, seed `useDataTableColumns` with server-persisted values. Use a `hydrated` ref to prevent the persist effect from immediately writing defaults back:
```typescript
const { effectiveColumns, columnsOrder, columnsWidth, setColumnsOrder, setMultipleColumnWidths } =
useDataTableColumns<MyRow>({ key: storeKey, columns });
const lastStoreKey = useRef(storeKey);
const hydrated = useRef(false);
useEffect(() => {
if (lastStoreKey.current !== storeKey) {
lastStoreKey.current = storeKey;
hydrated.current = false;
}
if (hydrated.current) return;
if (savedOrder.length > 0) setColumnsOrder(savedOrder);
if (Object.keys(savedWidths).length > 0) {
setMultipleColumnWidths(
Object.entries(savedWidths).map(([accessor, w]) => ({ accessor, width: w })),
);
}
requestAnimationFrame(() => { hydrated.current = true; });
}, [storeKey, savedOrder, savedWidths, setColumnsOrder, setMultipleColumnWidths]);
```
### Persist localStorage → server
Watch for changes and write back:
```typescript
const prevOrder = useRef(columnsOrder);
const prevWidths = useRef(columnsWidth);
useEffect(() => {
if (!hydrated.current) return;
const orderChanged = JSON.stringify(columnsOrder) !== JSON.stringify(prevOrder.current);
const widthsChanged = JSON.stringify(columnsWidth) !== JSON.stringify(prevWidths.current);
prevOrder.current = columnsOrder;
prevWidths.current = columnsWidth;
if (!orderChanged && !widthsChanged) return;
if (orderChanged) persistOption({ columnOrder: JSON.stringify(columnsOrder) });
if (widthsChanged) {
const widthMap: Record<string, number> = {};
for (const entry of columnsWidth) {
const key = Object.keys(entry)[0];
if (!key) continue;
const w = entry[key as keyof typeof entry];
if (typeof w === "number") widthMap[key] = w;
else if (typeof w === "string" && w.endsWith("px")) widthMap[key] = parseInt(w, 10);
}
persistOption({ columnWidths: JSON.stringify(widthMap) });
}
}, [columnsOrder, columnsWidth, persistOption]);
```
The `persistOption` helper calls both `setOptions` (local state) and `saveItemOptions` (server mutation).
---
## 5. Context Menu (Right-Click)
Do NOT install `mantine-contextmenu`. Use Mantine's `Menu` + `Portal` directly:
```typescript
interface ContextMenuState {
x: number;
y: number;
item: MyRow;
}
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
const handleContextMenu = useCallback(
({ record, event }: { record: MyRow; event: React.MouseEvent }) => {
event.preventDefault();
setContextMenu({ x: event.clientX, y: event.clientY, item: record });
},
[],
);
// On DataTable:
<DataTable onRowContextMenu={handleContextMenu} />
// Render:
{contextMenu && <RowContextMenu state={contextMenu} onClose={closeContextMenu} />}
```
The menu component uses `Portal` for correct z-index:
```tsx
function RowContextMenu({ state, onClose }: { state: ContextMenuState; onClose: () => void }) {
return (
<Portal>
<Menu opened onClose={onClose} closeOnItemClick={false} position="right-start" offset={0}>
<Menu.Target>
<Box style={{ position: "fixed", left: state.x, top: state.y, width: 0, height: 0 }} />
</Menu.Target>
<Menu.Dropdown>
<Menu.Item onClick={() => { /* action */ onClose(); }}>Action</Menu.Item>
</Menu.Dropdown>
</Menu>
</Portal>
);
}
```
**Key fix:** Use `closeOnItemClick={false}` if any menu item needs a two-step confirmation (like delete → confirm). Close the menu on scroll: `onScroll={() => { if (contextMenu) closeContextMenu(); }}`.
---
## 6. Row Expansion
```typescript
<DataTable
rowExpansion={{
trigger: "click",
allowMultiple: true,
collapseProps: {
transitionDuration: 200,
animateOpacity: true,
transitionTimingFunction: "ease-out",
},
content: ({ record, collapse }) => <ExpandedRow item={record} collapse={collapse} />,
}}
/>
```
**Critical:** Do NOT set `onRowClick` when using `rowExpansion` with `trigger: "click"`. They conflict and the expansion won't work.
---
## 7. Common Pitfalls & Fixes
### Status indicator dots
Don't use `<Badge variant="dot" />` without children — it renders a pill with empty space. Use a plain `Box`:
```tsx
<Box
w={8}
h={8}
style={{
borderRadius: "50%",
backgroundColor: `var(--mantine-color-${color}-filled)`,
flexShrink: 0,
}}
/>
```
### Tooltip overflow on long text columns
When a column has `ellipsis: true`, the tooltip target element can still extend beyond the cell bounds if inner elements don't clip. Fix:
```tsx
<Tooltip position="bottom-start" ...>
<Group style={{ overflow: "hidden" }}>
...
</Group>
</Tooltip>
```
Use `position="bottom-start"` instead of `position="right"` so the tooltip never escapes the widget horizontally.
### Dark mode tooltips
Mantine tooltips default to the theme's tooltip color which may be white in light mode and stay white in dark mode. Always set `color="dark"` on informational tooltips inside widgets.
### Division by zero
Guard all ratio calculations:
```typescript
if (item.size > 0) ratio = item.sent / item.size;
if (totalDown > 0) ratio = totalUp / totalDown;
```
### Composite idAccessor
When rows can come from multiple integrations, IDs may collide. Use a composite key:
```typescript
idAccessor={(record) => `${record.integration.id}:${record.id}`}
```
### JSON option parsing
Always wrap in try/catch:
```typescript
function parseJsonOption<T>(value: string | undefined, fallback: T): T {
if (!value) return fallback;
try { return JSON.parse(value) as T; }
catch { return fallback; }
}
```
### Clipboard API
`navigator.clipboard.writeText()` can throw in non-HTTPS contexts. Always catch:
```typescript
void navigator.clipboard.writeText(text).catch(() => {});
```
### Sync sortStatus with widget options
If the widget has configurable default sort, sync it:
```typescript
useEffect(() => {
setSortStatus({ columnAccessor: options.defaultSort, direction: defaultSortDirection });
}, [options.defaultSort, defaultSortDirection]);
```
---
## 8. Accessibility
- All `ActionIcon` components need `aria-label`
- Filter toggles (client badges, status badges) need `aria-pressed={isActive}`
- `textSelectionDisabled` on DataTable prevents accidental text selection during drag/resize
---
## 9. Error Handling
- Every mutation needs an `onError` callback with `showErrorNotification`
- The `saveItemOptions` mutation (column persistence) also needs `onError`
- Check `isError` on the main query and show a fallback UI
- Use a shared `mutationOptions` object to avoid repeating `onSettled`/`onError`:
```typescript
const mutationOptions = {
onSettled: () => void utils.myQuery.invalidate(),
onError: () => showErrorNotification({ title: t("errors.actionFailed"), message: t("errors.actionFailedMessage") }),
};
```
---
## 10. i18n
- No hardcoded strings. Every user-visible string goes through `useScopedI18n`
- Column titles: `t("items.{accessor}.columnTitle")`
- Detail labels: `t("items.{accessor}.detailsTitle")`
- States: `t("states.{state}")`
- Actions: `t("actions.{action}")`
- Errors: `t("errors.{errorKey}")`
- Hidden options still need `.label` translation keys
---
## 11. Edit Mode
Disable pointer events on the table during edit mode to prevent conflicts with the board's drag-and-drop:
```typescript
let tablePointerEvents: React.CSSProperties["pointerEvents"];
if (isEditMode) tablePointerEvents = "none";
let rowContextMenuHandler = handleContextMenu;
if (isEditMode) rowContextMenuHandler = undefined;
<DataTable style={{ pointerEvents: tablePointerEvents }} />
```
---
## 12. DataTable Props Reference (Commonly Used)
```typescript
<DataTable
records={sortedData}
columns={effectiveColumns}
storeColumnsKey={storeKey}
withTableBorder={false}
borderRadius={0}
highlightOnHover
striped="odd"
stripedColor={{ dark: "dark.7", light: "gray.0" }}
highlightOnHoverColor={{ dark: "dark.5", light: "gray.1" }}
verticalAlign="center"
fetching={isFetching && data.length === 0}
loaderBackgroundBlur={2}
fz={size.fontSize}
textSelectionDisabled
sortStatus={sortStatus}
onSortStatusChange={setSortStatus}
noRecordsText={t("errors.noItems")}
idAccessor={(record) => record.uniqueKey}
height="100%"
className="my-table"
defaultColumnProps={{
noWrap: true,
draggable: true,
resizable: true,
cellsStyle: () => ({ padding: `${size.cellPadding}px 8px` }),
}}
scrollAreaProps={{ type: "auto", scrollbarSize: 6 }}
rowBackgroundColor={(record) => conditionalBgMap[record.state]}
onRowContextMenu={handleContextMenu}
rowExpansion={{ ... }}
onScroll={() => { if (contextMenu) closeContextMenu(); }}
/>
```
---
## 13. Layout Stability
- Don't use `table-layout: fixed` — it fights mantine-datatable's resize logic
- Give non-name columns explicit `width` values so only the name column flexes
- Use `ellipsis: true` on the name column — it handles `text-overflow: ellipsis` internally
- Names should truncate at the cell boundary, not a character count, so the layout is stable regardless of content length
- `flexShrink: 0` on badges/icons prevents them from being squeezed by long names
---
## 14. Checklist for Each Migration
1. Replace imports (`mantine-react-table` → `mantine-datatable`)
2. Convert column definitions (`accessorKey` → `accessor`, `header` → `title`, `Cell` → `render`, etc.)
3. Implement manual sorting with `useState` + `useMemo`
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
+103
View File
@@ -0,0 +1,103 @@
---
description: How to expose tRPC procedures as MCP tools for AI clients
globs: packages/api/src/router/**/*.ts
alwaysApply: false
---
# MCP Integration Guide
Every new tRPC procedure that provides useful functionality should be exposed as an MCP tool so AI assistants (Claude, Cursor, etc.) can use it. This is a first-class feature of Homarr.
## Adding MCP to a procedure
Add `.meta({ mcp: { enabled: true, description: "..." } })` to the procedure chain:
```typescript
// Read-only tool (query)
myProcedure: protectedProcedure
.meta({ mcp: { enabled: true, description: "Get all items with their status and metadata" } })
.input(z.object({ limit: z.number().default(50) }))
.query(async ({ ctx, input }) => { ... })
// Write tool (mutation)
myAction: protectedProcedure
.meta({ mcp: { enabled: true, description: "Delete an item by ID. Requires interact permission on the integration" } })
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => { ... })
// No-input procedure (z.void) — works fine, no .input() needed
getAll: protectedProcedure
.meta({ mcp: { enabled: true, description: "List all resources" } })
.query(async ({ ctx }) => { ... })
```
The `.meta()` call must come **before** `.input()` and `.query()`/`.mutation()` in the chain.
## Writing good descriptions
The description is the **only thing** an AI sees to decide when and how to use the tool. Write it like you're explaining to a colleague who has never used Homarr.
**Must include:**
- What the tool returns or does
- Which integrations/services it works with (if applicable)
- How to get required IDs (e.g., "Use integration_all to get the integrationId")
- Permission requirements (if non-obvious)
**Good:**
```
"Get calendar events for upcoming and recent media releases. Fetches from all connected Sonarr (TV), Radarr (movies), Lidarr (music), and Readarr (books) integrations. Requires integrationIds from integration_all"
```
**Bad:**
```
"Get calendar events"
```
**For tools that return permission fields**, explain what they mean:
```
"List all integrations. Returns permissions.hasUseAccess (read) and permissions.hasInteractAccess (actions) — false means the API key lacks that permission, not an error"
```
## Registering in the MCP router
After adding `.meta()` to procedures, you must register the router in `packages/api/src/mcp.ts`. This file uses **eager imports** (no `lazy()`) because MCP tool extraction requires synchronous access to all procedure definitions.
```typescript
// packages/api/src/mcp.ts
import { myNewRouter } from "./router/my-new-feature";
export const mcpRouter = createTRPCRouter({
// ... existing routers
myFeature: myNewRouter,
});
```
**If your router is nested** (e.g., widget sub-routers), import and register the sub-router directly — do not import a parent that uses `lazy()`.
## What to expose vs. what to skip
**Expose:**
- Data queries (list, search, get by ID)
- Actions users would ask an AI to do (toggle, create, delete, approve)
- Status checks (health, stats, summaries)
**Skip:**
- Subscriptions (WebSocket-only, not supported by MCP)
- Internal procedures (session management, onboarding steps)
- File upload/download procedures
- Procedures that return non-serializable data (streams, blobs)
## Input schema rules
- Use `z.object({...})` for inputs — this maps cleanly to MCP tool parameters
- `z.void()` (no `.input()`) works — the MCP handler patches these to accept empty objects
- Avoid `z.union()` or `z.discriminatedUnion()` at the top level — AI clients struggle with these
- Use `.default()` on optional fields so the AI doesn't need to guess values
## Checklist for new features
1. Add `.meta({ mcp: { enabled: true, description: "..." } })` to relevant procedures
2. Import and register the router in `packages/api/src/mcp.ts` (eager, no lazy)
3. Write descriptions that explain what the tool does, what it needs, and what it returns
4. If the tool requires an ID from another tool, say which one in the description
5. Test with `curl` against `/api/mcp/mcp` to verify the tool appears in `tools/list`
+8
View File
@@ -0,0 +1,8 @@
version = 1
[[analyzers]]
name = "javascript"
[analyzers.meta]
plugins = ["react"]
environment = ["nodejs"]
+8 -14
View File
@@ -1,25 +1,22 @@
# Since .env is gitignored, you can use .env.example to build a new `.env` file when you clone the repo.
# Keep this file up-to-date when you add new variables to `.env`.
# Keep this file up-to-date when you add new variables to \`.env\`.
# This file will be committed to version control, so make sure not to have any secrets in it.
# If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets.
# Generate two different 64-character hex values with `openssl rand -hex 32` before starting Homarr.
# Never reuse these values or commit the populated `.env` file.
AUTH_SECRET=
# The below secret is not used anywhere but required for Auth.js (Would encrypt JWTs and Mail hashes, both not used)
AUTH_SECRET="supersecret"
# The below secret is used to encrypt integration secrets in the database.
# Homarr will reject an empty or invalid value and print a randomly generated candidate.
SECRET_ENCRYPTION_KEY=
# It should be a 32-byte string, generated by running `openssl rand -hex 32` on Unix
# or starting the project without any (which will show a randomly generated one).
SECRET_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
LOG_LEVEL='info'
# This is how you can use the sqlite driver:
DB_DRIVER='better-sqlite3'
# Set an absolute writable SQLite path so migrations and the app use the same file.
# Their working directories differ; do not use a relative path here.
# Container images use /appdata/db/db.sqlite by default instead.
DB_URL=
DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE'
# Those are the two ways to use the mysql2 driver:
# 1. Using the URL format:
@@ -66,14 +63,11 @@ UNSAFE_ENABLE_MOCK_INTEGRATION=true
# Public URL contract. Values must be absolute HTTP(S) URLs without credentials,
# query strings, or fragments. Trailing slashes are normalized at startup.
# Public Homarr origin used by MCP OAuth metadata and redirects behind proxies.
# BASE_URL=https://homarr.example.com
HOMARR_WEBSITE_URL=https://homarr.dev
# PocketBase API used by Homarr and by the standalone documentation site.
# Defaults to HOMARR_WEBSITE_URL.
# WORKSHOP_API_URL=https://homarr.dev
# Public Workshop website used by links in Homarr, Custom Widgets, onboarding,
# and assistant context. Defaults to HOMARR_WEBSITE_URL/workshop.
# Public Workshop page opened from Homarr. Defaults to HOMARR_WEBSITE_URL/workshop.
# WORKSHOP_WEB_URL=https://homarr.dev/workshop
# Documentation and PocketBase deployment settings.
-17
View File
@@ -25,18 +25,6 @@ body:
attributes:
label: Additional information
description: Add any other information related to the bug here, screenshots if applicable.
- type: input
id: exactBuild
attributes:
label: Exact build or image
description: For preview or development builds, include the commit SHA or image digest. A moving tag such as v2 is not enough to identify the build.
placeholder: "Commit SHA, image digest, or release version"
- type: input
id: lastWorkingVersion
attributes:
label: Last working version
description: If this worked before, include the last version or build where you tried the same action. Leave blank if unknown.
placeholder: "Version or commit SHA"
- type: dropdown
id: version
attributes:
@@ -45,11 +33,6 @@ body:
options:
# The below comment is used to insert a new version with on-release.yml
#NEXT_VERSION#
- 1.77.0
- 1.76.2
- 1.76.1
- 1.76.0
- 1.75.0
- 1.74.0
- 1.73.0
- 1.72.0
@@ -20,7 +20,7 @@ runs:
using: "composite"
steps:
- name: Prebuilt debian dependencies
uses: ./.github/actions/prebuilt-debian
uses: homarr-labs/homarr/.github/actions/prebuilt-debian@dev
id: prebuilt-debian
with:
architecture: ${{ inputs.architecture }}
@@ -11,6 +11,9 @@ outputs:
runs:
using: "composite"
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
Binary file not shown.

Before

Width:  |  Height:  |  Size: 582 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 649 KiB

+6 -6
View File
@@ -4,14 +4,14 @@
<h3>Homarr</h3>
</div>
**Thank you for your contribution. Please ensure that your pull request meets the following requirements:**
**Thank you for your contribution. Please ensure that your pull request meets the following pull request:**
- [ ] Relevant checks pass without warnings or errors
- [ ] Pull request targets the intended base branch (`dev` by default; `release/v2` for v2 release work)
- [ ] Builds without warnings or errors (`pnpm build`, autofix with `pnpm format:fix`)
- [ ] Pull request targets `dev` branch
- [ ] Commits follow the [conventional commits guideline](https://www.conventionalcommits.org/en/v1.0.0/)
- [ ] No shorthand variable names are used (for example, `x`, `y`, `i`, or abbreviations)
- [ ] User-facing changes include matching documentation updates under `apps/docs`
- [ ] When using AI, no temporary files are checked in and the code style follows the rest of the project
- [ ] No shorthand variable names are used (eg. `x`, `y`, `i` or any abbrevation)
- [ ] Documentation is up to date. Create a pull request [here](https://github.com/homarr-labs/documentation/).
- [ ] When using AI; No temp files are checked in, the code style follows the rest of the project
**When contributing a new integration or widget:**
+1 -1
View File
@@ -3,7 +3,7 @@ name: Formatting
on:
pull_request:
types: [opened, synchronize, reopened]
branches: ["**"]
branches: ["*"]
permissions:
contents: read
@@ -20,7 +20,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
client-id: ${{ vars.RENOVATE_APPROVE_CLIENT_ID }}
client-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
permission-pull-requests: write # required to enable auto-merge
- name: Install GitHub CLI
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
client-id: ${{ vars.RENOVATE_APPROVE_CLIENT_ID }}
client-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
permission-pull-requests: write # required to approve a pull request and enable auto-merge
- name: Install GitHub CLI
+81 -81
View File
@@ -3,12 +3,12 @@ name: CI
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
branches: ["**"]
branches: ["*"]
pull_request_target:
types: [closed]
branches: ["**"]
branches: ["*"]
push:
branches: [main, dev, beta, next, v2, release/v2]
branches: [main, dev, beta, next, v2]
merge_group:
workflow_dispatch:
inputs:
@@ -51,7 +51,7 @@ jobs:
name: Fast gate
if: github.event_name != 'pull_request_target' && (github.event_name != 'workflow_dispatch' || inputs.operation != 'cleanup')
runs-on: ubuntu-latest
timeout-minutes: 20
timeout-minutes: 15
env:
TURBO_API: ${{ secrets.TURBO_API }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
@@ -97,26 +97,26 @@ jobs:
- name: Validate Custom Widget bundle budget
run: |
pnpm turbo run build --filter=@homarr/nextjs
pnpm --filter nextjs build
pnpm check:custom-widget-bundle
- name: Run unit tests
run: pnpm test
container:
name: Container build
# Same-repository previews already build amd64; only build a fallback when no digest is available.
needs: [fast-gate, preview-amd64]
name: Container and E2E
needs: fast-gate
if: |
!cancelled() && needs.fast-gate.result == 'success' &&
needs.preview-amd64.result != 'failure' && needs.preview-amd64.outputs.digest == '' &&
(
github.event_name == 'merge_group' ||
(github.event_name == 'push' && github.ref == 'refs/heads/release/v2') ||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
(github.event_name == 'workflow_dispatch' && (inputs.operation == 'validate' || inputs.operation == 'publish'))
)
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
(github.event_name == 'workflow_dispatch' && (inputs.operation == 'validate' || inputs.operation == 'publish'))
runs-on: ubuntu-latest
timeout-minutes: 20
timeout-minutes: 30
permissions:
contents: read
packages: write
outputs:
digest: ${{ steps.preview-build.outputs.digest }}
steps:
- uses: actions/checkout@v7
if: "!env.ACT"
@@ -124,12 +124,15 @@ jobs:
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
- name: Setup
uses: ./tooling/github/setup
- name: Set up Docker Buildx
if: needs.preview-amd64.outputs.digest == '' && !env.ACT
if: "!env.ACT"
uses: docker/setup-buildx-action@v4
- name: Build container once
if: needs.preview-amd64.outputs.digest == '' && !env.ACT
if: "!env.ACT"
uses: docker/build-push-action@v7
with:
context: .
@@ -137,13 +140,15 @@ jobs:
platforms: linux/amd64
load: true
push: false
tags: homarr-ci
tags: |
homarr-ci
homarr-e2e
labels: |
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.ref.name=pr-${{ github.event.pull_request.number || inputs.pr_number || 'none' }}
cache-from: type=gha,scope=homarr-e2e
cache-to: type=gha,scope=homarr-e2e,mode=max
cache-from: type=gha,scope=homarr-linux-amd64
cache-to: type=gha,scope=homarr-linux-amd64,mode=max
secrets: |
TURBO_API=${{ secrets.TURBO_API }}
TURBO_TEAM=${{ secrets.TURBO_TEAM }}
@@ -165,46 +170,56 @@ jobs:
fi
done
DOCKER_BUILDKIT=1 docker build --platform linux/amd64 \
--tag homarr-ci "${secret_args[@]}" .
--tag homarr-ci --tag homarr-e2e "${secret_args[@]}" .
preview-amd64:
name: Build amd64 preview image
needs: fast-gate
if: |
(
(github.event_name == 'pull_request' && github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event_name == 'push' && github.ref == 'refs/heads/release/v2') ||
(github.event_name == 'workflow_dispatch' && inputs.operation == 'publish')
)
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
packages: write
outputs:
digest: ${{ steps.preview-build.outputs.digest }}
steps:
- uses: actions/checkout@v7
- name: Cache Playwright browsers
if: "!env.ACT"
id: playwright-cache
uses: actions/cache@v6
with:
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Log in to GHCR
if: "!env.ACT"
- name: Install Playwright Chromium
if: steps.playwright-cache.outputs.cache-hit != 'true' || env.ACT
run: pnpm exec playwright install --with-deps chromium
- name: Install Playwright system deps
if: steps.playwright-cache.outputs.cache-hit == 'true'
run: pnpm exec playwright install-deps chromium
- name: Run E2E tests
run: pnpm test:e2e
- name: Upload E2E artifacts
if: failure() && !env.ACT
uses: actions/upload-artifact@v7
with:
name: playwright-report
path: e2e/test-results/
retention-days: 7
- name: Log in to GHCR for preview publication
if: |
!env.ACT &&
(
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event_name == 'workflow_dispatch' && inputs.operation == 'publish')
)
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
if: "!env.ACT"
uses: docker/setup-buildx-action@v4
- name: Build and push amd64 preview image by digest
- name: Push amd64 preview image by digest
id: preview-build
if: "!env.ACT"
if: |
!env.ACT &&
(
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event_name == 'workflow_dispatch' && inputs.operation == 'publish')
)
uses: docker/build-push-action@v7
with:
context: .
@@ -212,20 +227,22 @@ jobs:
platforms: linux/amd64
outputs: type=image,"name=${{ env.PREVIEW_IMAGE }}",push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=homarr-linux-amd64
cache-to: type=gha,scope=homarr-linux-amd64,mode=max
secrets: |
TURBO_API=${{ secrets.TURBO_API }}
TURBO_TEAM=${{ secrets.TURBO_TEAM }}
TURBO_TOKEN=${{ secrets.TURBO_TOKEN }}
TURBO_REMOTE_CACHE_SIGNATURE_KEY=${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
- name: Show Act publication result
if: env.ACT
run: echo "Act validated the publication path; external GHCR writes are disabled locally."
preview-arm64:
name: Build arm64 preview image
needs: fast-gate
if: |
(
(github.event_name == 'pull_request' && github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event_name == 'push' && github.ref == 'refs/heads/release/v2') ||
(github.event_name == 'workflow_dispatch' && inputs.operation == 'publish')
)
runs-on: ubuntu-24.04-arm
@@ -273,15 +290,10 @@ jobs:
publish-preview:
name: Publish multi-platform preview image
needs: [fast-gate, preview-amd64, preview-arm64]
needs: [container, preview-arm64]
if: |
!cancelled() &&
needs.fast-gate.result == 'success' &&
needs.preview-amd64.result == 'success' &&
needs.preview-arm64.result == 'success' &&
(
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event_name == 'push' && github.ref == 'refs/heads/release/v2') ||
(github.event_name == 'workflow_dispatch' && inputs.operation == 'publish')
)
runs-on: ubuntu-latest
@@ -291,7 +303,7 @@ jobs:
packages: write
pull-requests: read
env:
PREVIEW_TAG: ${{ github.event_name == 'push' && 'v2' || format('pr-{0}', github.event.pull_request.number || inputs.pr_number) }}
PREVIEW_TAG: pr-${{ github.event.pull_request.number || inputs.pr_number }}
steps:
- name: Log in to GHCR
if: "!env.ACT"
@@ -328,28 +340,24 @@ jobs:
- name: Publish preview manifest
if: "!env.ACT"
env:
AMD64_DIGEST: ${{ needs.preview-amd64.outputs.digest }}
AMD64_DIGEST: ${{ needs.container.outputs.digest }}
ARM64_DIGEST: ${{ needs.preview-arm64.outputs.digest }}
run: |
tags=(-t "${PREVIEW_IMAGE}:${PREVIEW_TAG}")
docker buildx imagetools create "${tags[@]}" \
docker buildx imagetools create -t "${PREVIEW_IMAGE}:${PREVIEW_TAG}" \
"${PREVIEW_IMAGE}@${AMD64_DIGEST}" \
"${PREVIEW_IMAGE}@${ARM64_DIGEST}"
- name: Verify preview manifest platforms
if: "!env.ACT"
run: |
tags=("${PREVIEW_TAG}")
for tag in "${tags[@]}"; do
image="${PREVIEW_IMAGE}:${tag}"
manifest="$(docker buildx imagetools inspect "$image" --raw)"
for architecture in amd64 arm64; do
jq --exit-status --arg architecture "$architecture" \
'any(.manifests[]; .platform.os == "linux" and .platform.architecture == $architecture)' \
<<<"$manifest" >/dev/null
done
docker buildx imagetools inspect "$image"
image="${PREVIEW_IMAGE}:${PREVIEW_TAG}"
manifest="$(docker buildx imagetools inspect "$image" --raw)"
for architecture in amd64 arm64; do
jq --exit-status --arg architecture "$architecture" \
'any(.manifests[]; .platform.os == "linux" and .platform.architecture == $architecture)' \
<<<"$manifest" >/dev/null
done
docker buildx imagetools inspect "$image"
cleanup:
name: Clean up preview image
@@ -386,14 +394,6 @@ jobs:
core.info(`No package version found with tag "${tag}"`);
return;
}
const targetTags = target.metadata?.container?.tags ?? [];
const extraTags = targetTags.filter((targetTag) => targetTag !== tag);
if (extraTags.length > 0) {
core.warning(
`Package version ${target.id} also carries tag(s) ${extraTags.map((targetTag) => `"${targetTag}"`).join(", ")}; refusing cleanup.`,
);
return;
}
await github.rest.packages.deletePackageVersionForOrg({
package_type: "container",
package_name: packageName,
@@ -22,7 +22,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.CROWDIN_APP_PRIVATE_KEY }}
client-id: ${{ vars.CROWDIN_CLIENT_ID }}
client-id: ${{ secrets.CROWDIN_APP_ID }}
permission-contents: write # required to commit to crowdin branch
permission-pull-requests: write # required to create pull request
+10 -21
View File
@@ -16,10 +16,10 @@ permissions:
jobs:
create-and-merge-pr:
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 2
steps:
- name: Discord notification
if: ${{ inputs.send-notifications }}
if: ${{ github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
@@ -37,22 +37,11 @@ jobs:
branch: next
- name: Create pull request
id: create-pull-request
run: |
pr_url="$(gh pr list --base beta --head next --state open --json url --jq '.[0].url // empty')"
if [[ -z "$pr_url" ]]; then
pr_url="$(gh pr create \
--title "chore(release): release ${{ steps.semver.outputs.next }}-beta" \
--body "Detected change to be ${{ steps.semver.outputs.bump }}<br/>Bump version from ${{ steps.semver.outputs.current }} to ${{ steps.semver.outputs.next }}-beta" \
--base beta \
--head next \
--label automerge)"
fi
echo "url=$pr_url" >> "$GITHUB_OUTPUT"
echo "pr_number=${pr_url##*/}" >> "$GITHUB_OUTPUT"
run: 'gh pr create --title "chore(release): release ${{ steps.semver.outputs.next }}-beta" --body "Detected change to be ``${{ steps.semver.outputs.bump }}``<br/>Bump version from ``${{ steps.semver.outputs.current }}`` to ``${{ steps.semver.outputs.next }}-beta``" --base beta --head next --label automerge'
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
- name: Discord notification
if: ${{ inputs.send-notifications }}
if: ${{ github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
@@ -63,19 +52,19 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
client-id: ${{ vars.RENOVATE_APPROVE_CLIENT_ID }}
client-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
permission-pull-requests: write
- name: Approve PR
env:
GITHUB_TOKEN: ${{ steps.obtainApprovalToken.outputs.token }}
run: |
gh pr review "${{ steps.create-pull-request.outputs.url }}" --approve --body "Automatically approved by GitHub Action"
gh pr review --approve --body "Automatically approved by GitHub Action"
- name: Obtain token
id: obtainMergeToken
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
client-id: ${{ vars.RENOVATE_MERGE_CLIENT_ID }}
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # write to beta branch (due to merge)
permission-pull-requests: write # merge pull request
- id: automerge
@@ -93,21 +82,21 @@ jobs:
MERGE_REQUIRED_APPROVALS: 0 # do not require approvals
- name: Merged Discord notification
if: ${{ steps.automerge.outputs.mergeResult == 'merged' && inputs.send-notifications }}
if: ${{ steps.automerge.outputs.mergeResult == 'merged' && github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "Merged PR ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}-beta"
- name: Major Bump Discord notification
if: ${{ steps.semver.outputs.bump == 'major' && inputs.send-notifications }}
if: ${{ steps.semver.outputs.bump == 'major' && github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "The release PR must be manually merged because the next version is a major version: ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}-beta"
- name: Discord Fail Notification
if: ${{ failure() && inputs.send-notifications }}
if: failure() && github.event.inputs.send-notifications
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
+12 -64
View File
@@ -13,7 +13,7 @@ on:
type: boolean
required: false
default: true
description: Send notifications when redeploying an existing release
description: Send notifications
permissions:
contents: write # Required to update package.json version
@@ -38,11 +38,10 @@ jobs:
SKIP_RELEASE: ${{ github.event_name == 'workflow_dispatch' || github.ref_name == 'dev' || github.ref_name == 'next' }}
outputs:
version: ${{ steps.read-semver.outputs.version || steps.version-fallback.outputs.version }}
git_ref: ${{ steps.read-git-ref.outputs.ref || steps.read-semver.outputs.version || github.ref }}
git_ref: ${{ steps.read-git-ref.outputs.ref || github.ref }}
skipped: ${{ env.SKIP_RELEASE }}
upstream_sync: ${{ steps.update-upstream.outcome }}
steps:
- run: echo "Manual dispatch redeploys the latest existing release without creating a new one"
- run: echo "Skipping release for workflow_dispatch event"
if: env.SKIP_RELEASE == 'true'
# The below generated version fallback represents a normalized branch name, for example "feature/branch-name" -> "feature-branch-name"
- run: echo "version="$(echo ${{github.ref_name}} | sed 's/[^a-zA-Z0-9\-]/-/g') >> "$GITHUB_OUTPUT"
@@ -55,13 +54,12 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
client-id: ${{ vars.RENOVATE_MERGE_CLIENT_ID }}
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # required to commit package.json & changelog changes, merge them to dev / next and publish the release
- uses: actions/checkout@v7
if: env.SKIP_RELEASE == 'false' || github.ref_name == 'main' || github.ref_name == 'beta'
if: env.SKIP_RELEASE == 'false'
with:
fetch-depth: 0
persist-credentials: false
- uses: pnpm/action-setup@v6
if: env.SKIP_RELEASE == 'false'
@@ -70,29 +68,12 @@ jobs:
with:
node-version: 24.18.0
package-manager-cache: false
- name: Verify major release intent
- run: npm i -g pnpm
if: env.SKIP_RELEASE == 'false'
run: |
latest_tag="$(git describe --tags --abbrev=0)"
latest_major="${latest_tag#v}"
latest_major="${latest_major%%.*}"
package_version="$(jq --raw-output '.version' package.json)"
package_major="${package_version%%.*}"
if (( package_major <= latest_major )); then
exit 0
fi
if git log --format='%s%n%b' "${latest_tag}..HEAD" \
| grep --extended-regexp --quiet '(^[[:alnum:]-]+(\([^)]*\))?!:)|(^BREAKING[ -]CHANGE:)'; then
exit 0
fi
echo "::error::package.json declares ${package_version}, but commits since ${latest_tag} contain no breaking-change signal. Use a conventional ! commit or BREAKING CHANGE footer so Semantic Release creates the intended major version."
exit 1
- name: Install dependencies
if: env.SKIP_RELEASE == 'false'
run: pnpm install --frozen-lockfile
run: |
pnpm install
- name: Run Semantic Release
if: env.SKIP_RELEASE == 'false'
env:
@@ -116,10 +97,8 @@ jobs:
run: |
echo "ref=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Update dev / next branch
id: update-upstream
if: env.SKIP_RELEASE == 'false'
# Finish publishing the already-created release, then fail in the publish job if synchronization did not work.
continue-on-error: true
continue-on-error: true # Prevent pipeline from failing when merge fails
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
UPSTREAM_BRANCH: ${{ github.ref_name == 'main' && 'dev' || 'next' }}
@@ -226,14 +205,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
if: needs.release.outputs.skipped == 'false'
with:
ref: ${{ needs.release.outputs.git_ref }}
persist-credentials: false
- name: Extract amd64
if: needs.release.outputs.skipped == 'false'
uses: ./.github/actions/extract-build-artifact
uses: homarr-labs/homarr/.github/actions/extract-build-artifact@dev
with:
digest: "${{ env.GHCR_REPO }}@${{ needs.build-amd64.outputs.digest }}"
architecture: amd64
@@ -247,14 +221,9 @@ jobs:
runs-on: ubuntu-24.04-arm
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
if: needs.release.outputs.skipped == 'false'
with:
ref: ${{ needs.release.outputs.git_ref }}
persist-credentials: false
- name: Extract arm64
if: needs.release.outputs.skipped == 'false'
uses: ./.github/actions/extract-build-artifact
uses: homarr-labs/homarr/.github/actions/extract-build-artifact@dev
with:
digest: "${{ env.GHCR_REPO }}@${{ needs.build-arm64.outputs.digest }}"
architecture: arm64
@@ -277,7 +246,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
client-id: ${{ vars.RENOVATE_MERGE_CLIENT_ID }}
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # required to publish the release
- name: Log in to the Container registry
@@ -314,30 +283,9 @@ jobs:
run: |
gh release edit --repo ${{ github.repository }} ${{ needs.release.outputs.version }} --draft=false
- name: Verify dev / next synchronization
if: needs.release.outputs.skipped == 'false' && needs.release.outputs.upstream_sync != 'success'
run: |
echo "::error::The released commit was not synchronized back to the upstream development branch."
exit 1
- name: Discord notification
if: github.event_name != 'workflow_dispatch' || inputs['send-notifications']
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "Successfully deployed images for branch **${{ github.ref_name }}**. Tagged as **${{env.NEXT_VERSION}}**."
notify-failure:
name: Notify deployment failure
if: ${{ always() && (failure() || cancelled()) && (github.event_name != 'workflow_dispatch' || inputs['send-notifications']) }}
needs: [release, build-amd64, build-arm64, extract-asset-amd64, extract-asset-arm64, publish]
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Discord failure notification
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "The deployment for branch **${{ github.ref_name }}** failed or was cancelled: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
+11 -31
View File
@@ -19,10 +19,10 @@ permissions:
jobs:
create-and-merge-pr:
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 2
steps:
- name: Discord notification
if: ${{ github.event_name == 'schedule' || inputs.send-notifications }}
if: ${{ github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
@@ -38,30 +38,13 @@ jobs:
with:
token: ${{ github.token }}
branch: dev
noNewCommitBehavior: silent
noVersionBumpBehavior: silent
- name: No release needed
if: ${{ steps.semver.outputs.bump == '' || steps.semver.outputs.bump == 'none' }}
run: echo "No release-worthy commits were found since ${{ steps.semver.outputs.current }}." >> "$GITHUB_STEP_SUMMARY"
- name: Create pull request
id: create-pull-request
if: ${{ steps.semver.outputs.bump != '' && steps.semver.outputs.bump != 'none' }}
run: |
pr_url="$(gh pr list --base main --head dev --state open --json url --jq '.[0].url // empty')"
if [[ -z "$pr_url" ]]; then
pr_url="$(gh pr create \
--title "chore(release): automatic release ${{ steps.semver.outputs.next }}" \
--body "**This is an automatic release**.<br/>Manual action may be required for major bumps.<br/>Detected change to be ``${{ steps.semver.outputs.bump }}``<br/>Bump version from ``${{ steps.semver.outputs.current }}`` to ``${{ steps.semver.outputs.next }}``" \
--base main \
--head dev \
--label automerge)"
fi
echo "url=$pr_url" >> "$GITHUB_OUTPUT"
echo "pr_number=${pr_url##*/}" >> "$GITHUB_OUTPUT"
run: 'gh pr create --title "chore(release): automatic release ${{ steps.semver.outputs.next }}" --body "**This is an automatic release**.<br/>Manual action may be required for major bumps.<br/>Detected change to be ``${{ steps.semver.outputs.bump }}``<br/>Bump version from ``${{ steps.semver.outputs.current }}`` to ``${{ steps.semver.outputs.next }}``" --base main --head dev --label automerge'
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
- name: Discord notification
if: ${{ steps.create-pull-request.outcome == 'success' && (github.event_name == 'schedule' || inputs.send-notifications) }}
if: ${{ github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
@@ -69,29 +52,26 @@ jobs:
args: "Created a release PR ${{ steps.create-pull-request.outputs.url }} for version ${{ steps.semver.outputs.next }} (new behaviour: ${{ steps.semver.outputs.bump }})"
- name: Obtain token
id: obtainApprovalToken
if: ${{ steps.create-pull-request.outcome == 'success' }}
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
client-id: ${{ vars.RENOVATE_APPROVE_CLIENT_ID }}
client-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
permission-pull-requests: write
- name: Approve PR
if: ${{ steps.create-pull-request.outcome == 'success' }}
env:
GITHUB_TOKEN: ${{ steps.obtainApprovalToken.outputs.token }}
run: |
gh pr review "${{ steps.create-pull-request.outputs.url }}" --approve --body "Automatically approved by GitHub Action"
gh pr review --approve --body "Automatically approved by GitHub Action"
- name: Obtain token
id: obtainMergeToken
if: ${{ steps.create-pull-request.outcome == 'success' }}
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
client-id: ${{ vars.RENOVATE_MERGE_CLIENT_ID }}
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # write to main branch (due to merge)
permission-pull-requests: write # merge pull request
- id: automerge
if: ${{ steps.create-pull-request.outcome == 'success' && steps.semver.outputs.bump != 'major' }}
if: ${{ steps.semver.outputs.bump != 'major' }}
name: automerge
uses: "pascalgn/automerge-action@v0.16.4"
env:
@@ -105,21 +85,21 @@ jobs:
MERGE_REQUIRED_APPROVALS: 0 # do not require approvals
- name: Merged Discord notification
if: ${{ steps.automerge.outputs.mergeResult == 'merged' && (github.event_name == 'schedule' || inputs.send-notifications) }}
if: ${{ steps.automerge.outputs.mergeResult == 'merged' && github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "Merged PR ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}"
- name: Major Bump Discord notification
if: ${{ steps.semver.outputs.bump == 'major' && (github.event_name == 'schedule' || inputs.send-notifications) }}
if: ${{ steps.semver.outputs.bump == 'major' && github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "The release PR must be manually merged because the next version is a major version: ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}"
- name: Discord Fail Notification
if: ${{ failure() && (github.event_name == 'schedule' || inputs.send-notifications) }}
if: failure() && github.event.inputs.send-notifications
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
+30 -2
View File
@@ -26,8 +26,36 @@ jobs:
- name: Copy env
run: cp .env.example .env
- name: Generate and validate OpenAPI spec
run: pnpm scripts:update-openapi
- name: Start dev server
run: pnpm --filter @homarr/nextjs dev &
env:
TURBO_TELEMETRY_DISABLED: 1
DISABLE_TELEMETRY: 1
- name: Wait for server and fetch spec
run: |
set -euo pipefail
for i in $(seq 1 30); do
if curl -fsS http://localhost:3000/api/openapi -o /tmp/openapi.json 2>/dev/null; then
echo "Server is up, spec fetched"
break
fi
echo "Waiting for server... ($i/30)"
sleep 3
done
if [ ! -f /tmp/openapi.json ]; then
echo "::error::Server failed to start within timeout"
exit 1
fi
jq -e '.openapi and .paths' /tmp/openapi.json > /dev/null || {
echo "::error::Fetched file is not a valid OpenAPI spec"
exit 1
}
cp /tmp/openapi.json apps/docs/static/api/open-api-schema.json
echo "OpenAPI spec updated ($(jq '.paths | length' /tmp/openapi.json) paths)"
- name: Commit updated spec
uses: EndBug/add-and-commit@v10
@@ -5,7 +5,7 @@ permissions:
on:
pull_request:
branches: ["**"]
branches: ["*"]
paths: [".github/actions/prebuilt-debian/**", "deployments/prebuilt-debian/**"]
jobs:
@@ -5,7 +5,7 @@ permissions:
on:
pull_request:
branches: ["**"]
branches: ["*"]
paths: [".github/renovate.json5"]
jobs:
+34 -1
View File
@@ -5,6 +5,39 @@ on:
types: [released]
jobs:
trigger-docs-release:
name: Trigger Documentation Release
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Obtain token
id: obtainToken
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.HOMARR_DOCS_RELEASE_APP_PRIVATE_KEY }}
client-id: ${{ vars.HOMARR_DOCS_RELEASE_APP_ID }}
owner: homarr-labs
repositories: |
documentation
permission-contents: write # required to dispatch repository workflow
- name: Trigger documentation release
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
SOURCE_TAG: ${{ github.event.release.tag_name }}
run: |
curl -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Accept: application/vnd.github+json" \
https://api.github.com/repos/homarr-labs/documentation/dispatches \
-d @- <<EOF
{
"event_type": "trigger-release",
"client_payload": {
"tag": "${SOURCE_TAG}"
}
}
EOF
update-bug-report-template:
name: Update Bug Report Template
runs-on: ubuntu-latest
@@ -15,7 +48,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
client-id: ${{ vars.RENOVATE_MERGE_CLIENT_ID }}
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # required to commit to branch
permission-pull-requests: write # required to create pr & enable automerge
- name: Checkout code
@@ -1,6 +1,5 @@
permissions:
statuses: write
contents: read
on:
pull_request:
@@ -17,20 +16,12 @@ jobs:
env:
GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
run: |
for attempt in 1 2 3 4 5; do
if gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
/repos/${{github.repository}}/statuses/${{github.event.pull_request.head.sha}} \
-f 'state=success' \
-f 'target_url=https://docs.renovatebot.com/configuration-options#minimumreleaseage' \
-f 'description=Skipped for non renovate branches' \
-f 'context=renovate/stability-days' \
--silent; then
exit 0
fi
echo "status POST failed (attempt $attempt/5), retrying in 5s"
sleep 5
done
exit 1
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
/repos/${{github.repository}}/statuses/${{github.event.pull_request.head.sha}} \
-f 'state=success' \
-f 'target_url=https://docs.renovatebot.com/configuration-options#minimumreleaseage' \
-f 'description=Skipped for non renovate branches' \
-f 'context=renovate/stability-days'
+6 -3
View File
@@ -15,13 +15,16 @@ jobs:
update-contributors:
runs-on: ubuntu-latest
timeout-minutes: 2
strategy:
matrix:
node-version: [22]
steps:
- name: Obtain token
id: obtainToken
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
client-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_CLIENT_ID }}
client-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
permission-contents: write # required to commit to branch
permission-pull-requests: write # required to create pr & enable automerge
@@ -31,10 +34,10 @@ jobs:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
- uses: pnpm/action-setup@v6
- name: Use Node.js
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6
with:
node-version: 24.18.0
node-version: ${{ matrix.node-version }}
cache: "pnpm"
- name: Run update script
@@ -24,7 +24,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
client-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_CLIENT_ID }}
client-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
permission-contents: write # required to commit to branch
permission-pull-requests: write # required to create pr & enable automerge
- name: Checkout code
+34 -134
View File
@@ -2,162 +2,62 @@ name: Workshop
on:
pull_request:
paths:
- ".github/workflows/workshop.yml"
- "apps/workshop/**"
- "apps/docs/**"
- "packages/custom-widgets/**"
- "packages/definitions/**"
- "packages/workshop/**"
- "tooling/typescript/**"
- "patches/**"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "turbo.json"
- ".npmrc"
push:
branches: [dev, v2, release/v2, main]
paths:
- ".github/workflows/workshop.yml"
- "apps/workshop/**"
- "apps/docs/**"
- "packages/custom-widgets/**"
- "packages/definitions/**"
- "packages/workshop/**"
- "tooling/typescript/**"
- "patches/**"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "turbo.json"
- ".npmrc"
branches: [dev, v2, main]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ (github.ref_name == 'v2' || github.ref_name == 'release/v2') && 'v2-preview' || github.ref }}
cancel-in-progress: true
jobs:
validate:
runs-on: ubuntu-24.04
timeout-minutes: 5
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with:
node-version: 24.18.0
- uses: pnpm/action-setup@v6
with:
run_install: false
- run: pnpm install --frozen-lockfile
- run: pnpm --filter @homarr/workshop typecheck
- run: pnpm --filter @homarr/docs typecheck
- run: docker compose --env-file apps/workshop/.env.staging.example -f apps/workshop/docker-compose.staging.yml config --quiet
publish-amd64:
if: github.event_name == 'push' && (github.ref_name == 'v2' || github.ref_name == 'release/v2' || github.ref_name == 'main')
needs: validate
runs-on: ubuntu-24.04
timeout-minutes: 20
permissions:
contents: read
packages: write
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v7
- uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v7
id: build
with:
context: .
file: apps/workshop/Dockerfile
target: production
platforms: linux/amd64
outputs: type=image,"name=ghcr.io/homarr-labs/workshop",push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=workshop-linux-amd64
cache-to: type=gha,scope=workshop-linux-amd64,mode=max
publish-arm64:
if: github.event_name == 'push' && (github.ref_name == 'v2' || github.ref_name == 'release/v2' || github.ref_name == 'main')
needs: validate
runs-on: ubuntu-24.04-arm
timeout-minutes: 20
permissions:
contents: read
packages: write
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v7
- uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v7
id: build
with:
context: .
file: apps/workshop/Dockerfile
target: production
platforms: linux/arm64
outputs: type=image,"name=ghcr.io/homarr-labs/workshop",push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=workshop-linux-arm64
cache-to: type=gha,scope=workshop-linux-arm64,mode=max
- run: pnpm test:workshop
- run: pnpm test:workshop-image
publish:
if: github.event_name == 'push' && (github.ref_name == 'v2' || github.ref_name == 'release/v2' || github.ref_name == 'main')
needs: [publish-amd64, publish-arm64]
if: github.event_name == 'push' && (github.ref_name == 'v2' || github.ref_name == 'main')
needs: validate
runs-on: ubuntu-24.04
timeout-minutes: 5
timeout-minutes: 30
permissions:
contents: read
packages: write
env:
WORKSHOP_IMAGE: ghcr.io/homarr-labs/workshop
steps:
- uses: actions/checkout@v7
- uses: docker/setup-qemu-action@v4
- uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Verify branch head before publication
uses: actions/github-script@v8
- uses: docker/metadata-action@v6
id: metadata
with:
script: |
const branchName = context.ref.replace("refs/heads/", "");
const { data: branch } = await github.rest.repos.getBranch({
owner: context.repo.owner,
repo: context.repo.repo,
branch: branchName,
});
if (branch.commit.sha !== context.sha) {
core.setFailed(`Branch ${branchName} moved from ${context.sha} to ${branch.commit.sha}.`);
}
- name: Publish multi-platform image
env:
AMD64_DIGEST: ${{ needs.publish-amd64.outputs.digest }}
ARM64_DIGEST: ${{ needs.publish-arm64.outputs.digest }}
run: |
tags=(-t "${WORKSHOP_IMAGE}:sha-${GITHUB_SHA}")
if [[ "${GITHUB_REF_NAME}" == "v2" || "${GITHUB_REF_NAME}" == "release/v2" ]]; then
tags+=(-t "${WORKSHOP_IMAGE}:v2")
fi
if [[ "${GITHUB_REF_NAME}" == "main" ]]; then
tags+=(-t "${WORKSHOP_IMAGE}:latest")
fi
docker buildx imagetools create "${tags[@]}" \
"${WORKSHOP_IMAGE}@${AMD64_DIGEST}" \
"${WORKSHOP_IMAGE}@${ARM64_DIGEST}"
- name: Verify multi-platform image
run: |
image="${WORKSHOP_IMAGE}:sha-${GITHUB_SHA}"
manifest="$(docker buildx imagetools inspect "$image" --raw)"
for architecture in amd64 arm64; do
jq --exit-status --arg architecture "$architecture" \
'any(.manifests[]; .platform.os == "linux" and .platform.architecture == $architecture)' \
<<<"$manifest" >/dev/null
done
docker buildx imagetools inspect "$image"
images: ghcr.io/homarr-labs/workshop
tags: |
type=sha,prefix=sha-,format=long
type=raw,value=v2,enable=${{ github.ref_name == 'v2' }}
type=raw,value=latest,enable=${{ github.ref_name == 'main' }}
- uses: docker/build-push-action@v7
with:
context: .
file: apps/workshop/Dockerfile
target: production
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
+3 -5
View File
@@ -25,8 +25,6 @@ packages/db/migrations/*/migrate.cjs
# production
build
tools/homarr-dev/homarr
tools/homarr-dev/homarr-dev
# misc
.DS_Store
@@ -76,6 +74,9 @@ apps/nextjs/public/images/background.png
.opencode
.agents/*
!.agents/skills/
.agents/skills/*
!.agents/skills/homarr-custom-widget/
!.agents/skills/homarr-custom-widget/**
skills-lock.json
# docusaurus
@@ -91,6 +92,3 @@ homarr-backup*.zip
*.sqlite-wal
*.sqlite-shm
/scripts/benchmarks/
# PR description screenshots belong on the pull request, not in the repository
.screenshots/
@@ -0,0 +1,116 @@
---
target: Workshop store in the docs
total_score: 20
p0_count: 1
p1_count: 3
timestamp: 2026-07-21T11-32-50Z
slug: apps-docs-src-components-workshop
---
# Workshop Store Critique
## Design Health Score
| # | Heuristic | Score | Key issue |
| --------- | ----------------------------------- | --------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | Visibility of system status | 1/4 | Fetch failure can show beside an empty-store message, so the system communicates two incompatible states. |
| 2 | Match between system and real world | 3/4 | Widget, CSS, voting, revisions, and comments use familiar language. |
| 3 | User control and freedom | 2/4 | Filters are reversible, but the crashed route and failed fetch state offer no useful recovery. |
| 4 | Consistency and standards | 2/4 | The submission editor uses CodeMirror, while revision editing falls back to a textarea; several tiny custom controls do not follow the shared button vocabulary. |
| 5 | Error prevention | 2/4 | Submission validation exists, but disabled progression does not explain what is missing and destructive actions use a brief inline confirmation. |
| 6 | Recognition rather than recall | 3/4 | Labels, type choices, and the submission stepper are understandable without documentation. |
| 7 | Flexibility and efficiency | 2/4 | Search covers titles only, signed-out users can select Yours, and installation is reduced to ambiguous Copy and Download actions. |
| 8 | Aesthetic and minimalist design | 3/4 | The restrained visual system is clean, but the browse toolbar and card action rows become dense. |
| 9 | Help users recover from errors | 0/4 | The production route crashes with React error 130; the fetch error has no retry action or useful context. |
| 10 | Help and documentation | 2/4 | The editor explains its fields, but the store does not teach the full inspect, trust, download, and install journey. |
| **Total** | | **20/40** | **Needs work before release** |
## Anti-Patterns Verdict
The surface does not look like obvious AI-generated decoration. Its visual restraint, familiar controls, and practical editor are credible product UI. The weakness is product behavior rather than styling: contradictory states, tiny action controls, an overloaded responsive toolbar, and a primary installation journey that is not explicit.
The deterministic Impeccable scan returned zero findings for `apps/docs/src/components/workshop`. This is useful but incomplete. The detector did not catch runtime failures, semantic state conflicts, touch-target sizing, or responsive grouping. Browser evidence found a full route crash, React error 130, and an earlier state that rendered both `Something went wrong.` and `No submissions yet`.
No visual overlay was injected because the available browser evaluation surface is read-only. Screenshots, DOM snapshots, console logs, and source inspection were used instead.
## Overall Impression
The submission modal is now one of the stronger parts of the Workshop. The store around it is not release-ready. The single biggest opportunity is to make discovery and installation a dependable, explicit path: load reliably, show one truthful state, help users evaluate a submission, and give them one obvious next action.
## What Is Working
1. The visual system is restrained and consistent with the Homarr documentation. Typography, color, radii, and spacing do not compete with the content.
2. The submission flow chunks a complex task into Type, Details, and Media. The shared JSON/CSS editor, drag-and-drop import, metadata autofill, and capped media previews reduce creator effort.
3. The detail design exposes revision, author, reports, changelog, source content, and comments. This supports inspection rather than treating community code as a blind install.
## Priority Issues
### P0: The Workshop route crashes in the production docs build
**Why it matters:** No visual polish matters if the store becomes a Docusaurus error page. A fresh independent tab consistently reached `This page crashed` with React error 130. The console attributed the thrown error to the Kapa widget bundle.
**Fix:** Isolate or disable the Kapa script on Workshop routes, prevent third-party widget failures from reaching the Docusaurus route boundary, and add a route-level Workshop boundary that preserves navigation and retry. Reproduce against the built Docker docs image, not only the development server.
**Suggested command:** `$impeccable harden`
### P1: Failure and empty states contradict each other
**Why it matters:** Showing `Something went wrong.` followed by `No submissions yet` makes users believe the store is both broken and empty. The generic message gives no cause and no recovery.
**Fix:** Make loading, failure, empty, and results mutually exclusive. The failure state should say that listings could not be loaded, preserve the current filters, and provide `Try loading again`. Do not render filters or an empty store as authoritative data after the initial request fails.
**Suggested command:** `$impeccable harden`
### P1: Mobile discovery controls are too dense and do not form a responsive hierarchy
**Why it matters:** The toolbar's inner row contains four type choices, sorting, and outdated visibility without wrapping. Most controls are 24 to 28 pixels tall. At narrow widths this is difficult to scan, easy to clip, and below a comfortable touch target.
**Fix:** Put a full-width search first on mobile, keep All, Widgets, and CSS as the primary segmented choice, move sorting and outdated visibility into a compact secondary row or popover, and hide Yours when signed out or make it a sign-in affordance. Use at least 40 to 44 pixel touch targets. Add `aria-pressed` or proper tab semantics to the type filter.
**Suggested command:** `$impeccable adapt`
### P1: The store does not make the install journey obvious
**Why it matters:** A first-time user sees Copy and Download without knowing which is recommended, what will be copied, how to inspect safety, or where the file goes in Homarr. This weakens the store's main purpose.
**Fix:** Make the detail page the decision surface. Use one primary action named for its result, such as `Download widget JSON` or `Copy CSS`, with a short `Install in Homarr` explanation linked to the correct in-app flow. Cards should prioritize title, preview, compatibility, score, and `View details`; secondary utilities should not compete with evaluation.
**Suggested command:** `$impeccable clarify`
### P2: Card and moderation actions are too small and too numerous
**Why it matters:** Vote, copy, download, comment, delete, and report controls compete in a compact footer. Low-contrast icon buttons are hard to discover and hard to use by touch. CSS columns also create a top-to-bottom visual reading order that can differ from the expected row order.
**Fix:** Use a regular responsive grid, remove hover scaling, keep one or two visible card actions, and move owner/moderation actions to a menu. Keep voting visible but increase its target size. Correct the report copy: reports and reporter identities are moderation data, not public social proof.
**Suggested command:** `$impeccable distill`
## Persona Red Flags
### Jordan, first-time self-hoster
Jordan opens Workshop to install a widget. The route can crash before any listing appears. In the partial failure state, Jordan sees both an error and an empty store. If listings load, Copy and Download do not explain the Homarr destination or recommended path. Jordan is likely to leave before installing anything.
### Alex, experienced Homarr administrator
Alex can understand the JSON and appreciates the source viewer, but title-only search and limited filtering make a larger catalog inefficient. Compatibility and required secret information are not prominent at browse time. Tiny moderation and owner controls slow repeated management tasks.
### Morgan, mobile user
Morgan encounters a toolbar with more controls than fit comfortably in one decision group, 24 to 28 pixel targets, and a signed-out Yours filter that leads to an unhelpful no-results state. Submission cards add another dense row of small actions.
## Minor Observations
- Future submission steps are focusable buttons even when clicking them does nothing. Disable them or render non-interactive progress markers.
- The report textarea relies on placeholder text and needs a persistent accessible label.
- The revision editor should reuse the same JSON/CSS CodeMirror component as creation.
- Search should include title, description, and author at minimum.
- The timed inline `Confirm?` deletion pattern is easy to miss and weak for keyboard and touch users.
- The detail-page changelog uses an uppercase tracked label, which is unnecessary in this otherwise restrained interface.
- Loading comments should use the same skeleton vocabulary as listings rather than centered status text.
## Questions to Consider
1. Is Workshop primarily a catalog for discovery, or a technical exchange for copying source? The primary card and detail actions should commit to one answer.
2. Should users evaluate compatibility and required secrets before opening a listing, or only on the detail page?
3. Does Workshop need every moderation and ownership action visible inline, or should those actions live behind a menu?
@@ -0,0 +1,64 @@
---
target: Workshop store in the docs
total_score: 38
p0_count: 0
p1_count: 0
timestamp: 2026-07-21T12-29-41Z
slug: apps-docs-src-components-workshop
---
# Workshop Store Critique — Post-Polish
## Design Health Score
| # | Heuristic | Score | Evidence |
| --------- | ----------------------------------- | --------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | Visibility of system status | 4/4 | Loading, initial failure, stale refresh failure, empty, and results states are mutually exclusive; recovery actions are visible. |
| 2 | Match between system and real world | 4/4 | Actions describe their result: Download widget JSON, Copy CSS, View details, and Install in Homarr. |
| 3 | User control and freedom | 4/4 | Filters can be cleared, errors retried, dialogs cancelled, and destructive submission deletion requires explicit confirmation. |
| 4 | Consistency and standards | 4/4 | Creation and revision editing share CodeMirror, buttons follow the shared system, and cards use a predictable responsive grid. |
| 5 | Error prevention | 3/4 | Validation, disabled progression, pending states, RLS, and confirmations are strong; disabled form progression could explain unmet requirements more explicitly. |
| 6 | Recognition rather than recall | 4/4 | Persistent labels, type descriptions, step status, source previews, and installation instructions minimize recall. |
| 7 | Flexibility and efficiency | 4/4 | Search spans title, description, and author; signed-in filtering, sorting, responsive controls, and direct detail actions support repeat use. |
| 8 | Aesthetic and minimalist design | 3/4 | The catalog is restrained and scannable; long source documents and the complete creator form necessarily remain dense on small screens. |
| 9 | Help users recover from errors | 4/4 | Route, listing, edit, report, copy, OAuth, and detail failures remain local and provide a clear next action. |
| 10 | Help and documentation | 4/4 | The detail page explains the exact Homarr destination and links to updated Workshop documentation. |
| **Total** | | **38/40** | **Release-quality; minor polish remains** |
## Anti-Patterns Verdict
The surface is product-led rather than decorative. The redesign removes the overloaded card footer, hover scaling, tiny touch controls, public moderation details, contradictory states, and ambiguous install actions. The deterministic Impeccable detector reports zero findings.
## What Improved
1. The production route no longer collapses into the Docusaurus crash screen without recovery. A route-local boundary protects navigation, and the fresh-volume production path renders without console errors.
2. Loading, failure, empty, stale-results, and populated states are truthful and mutually exclusive.
3. Search and filters form a clear responsive hierarchy. Mobile targets are 40–44 pixels, signed-out users do not see Yours, and cards no longer overflow at 390 pixels.
4. Cards prioritize evaluation: title, author, type, score, description, comments, and View details. Copy, download, reporting, editing, and deletion moved to the detail page.
5. The detail page now teaches the installation journey and names its primary action precisely.
6. Reports and reporter identities are admin-only through PocketBase rules. Public catalog UI no longer turns reports into social proof.
7. The creator and revision dialogs share the CodeMirror editor, use bounded internal scrolling, expose pending/error states, and avoid step-selection layout shifts.
## Remaining Minor Opportunities
### P2: Explain disabled progression inline
The Type and Details steps are understandable, but a disabled Next button could state the missing requirement for keyboard and first-time users. This is a refinement, not a blocker, because required fields and validation messages are already present.
### P2: Consider a compact source summary on very small screens
The read-only source viewer is intentionally complete and horizontally scrollable. A future collapse/expand summary could reduce page length for nontechnical visitors without hiding inspection entirely.
## Verification Evidence
- Fresh Docker Compose PocketBase volume and production docs image.
- Populated catalog with widget, CSS, and outdated examples.
- Desktop and 390px mobile layouts in light and dark themes.
- Widget detail, installation instructions, CodeMirror source view, and comments.
- Signed-in submission wizard through Type and Details, including the shared editor.
- Zero browser console errors after the final fixes.
- Impeccable deterministic detector: zero findings.
## Compatibility Note
An existing local development volume created from an earlier unreleased version of the reset migration still has a stale Workshop view. It is intentionally not migrated or deleted. A fresh volume created from the current unreleased migration is the supported validation target and works correctly.
@@ -0,0 +1,106 @@
---
target: Homarr Workshop docs UI
total_score: 27
p0_count: 0
p1_count: 2
timestamp: 2026-07-22T15-58-15Z
slug: apps-docs-src-components-workshop
---
## Design Health Score
| # | Heuristic | Score | Key Issue |
| --------- | ------------------------------- | --------: | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | Visibility of System Status | 3 | Strong loading, retry, stale-data, pending, and copy states; successful report/edit actions still end silently. |
| 2 | Match System / Real World | 3 | Core Workshop language is clear, but `networkScope`, query/action, and schema terminology assumes technical fluency. |
| 3 | User Control and Freedom | 3 | Back, cancel, clear filters, vote toggling, and destructive confirmations are present; destructive actions have no undo. |
| 4 | Consistency and Standards | 3 | Shared primitives are coherent, though Docusaurus chrome and the denser app-store vocabulary still feel slightly stitched together. |
| 5 | Error Prevention | 2 | Validation and confirmations are good, but install safety and credential context appears after download/copy actions on small screens. |
| 6 | Recognition Rather Than Recall | 3 | Author, revision, source, filters, and capabilities are visible; installation still asks users to remember a separate Homarr navigation path. |
| 7 | Flexibility and Efficiency | 2 | Search, filters, sort, drag-and-drop, autofill, and comment shortcuts help; the install handoff remains manual. |
| 8 | Aesthetic and Minimalist Design | 2 | Hierarchy is clear, but repeated panels, badges, uppercase metadata labels, and overlapping install actions add noise. |
| 9 | Error Recognition and Recovery | 3 | Errors are plain-language and usually preserve work; the global action error can be detached from the failed control. |
| 10 | Help and Documentation | 3 | The installation guide and source explanation are useful; security terminology lacks inline definitions. |
| **Total** | | **27/40** | **Acceptable, significant improvements needed** |
## Anti-Patterns Verdict
**LLM assessment:** Moderate AI-assisted feel, not outright slop. Domain-specific copy, extensive state coverage, and transparent source/capability information make the feature credible. The generic card grid, repeated rounded panels, icon tiles, uppercase tracked labels, and decorative changelog stripe still expose a component-recipe grammar. It looks competent but not fully edited into a distinctive Homarr store.
**Deterministic scan:** `detect.mjs --json apps/docs/src/components/workshop` exited 0 with exact output `[]`: zero findings, zero triggered rules, and no false positives. The scan found none of Impeccable's deterministic anti-patterns.
**Visual overlays:** No reliable user-visible overlay is available. The independent detector agent had no browser backend. The parent fallback opened a fresh browser tab and visually inspected the listing, but the browser API did not expose a mutable evaluation method for the required preflight, so injection and `[Human]` visibility were correctly skipped. Browser/source evidence was used instead.
## Overall Impression
The Workshop is already substantially better than a generic gallery: it communicates authorship, source code, API capabilities, secrets, revision history, reports, and discussion. The biggest opportunity is to turn that information into a deliberate trust sequence. Today the interface lets the user act first and understand safety later, especially on mobile.
## What's Working
- **State coverage is unusually strong.** Listing failures, stale data, loading skeletons, empty results, filter recovery, comment errors, pending actions, and destructive confirmations have explicit UI.
- **Pre-install transparency is excellent.** API hosts, methods, configurable options, network scope, and credential requirements are visible instead of hidden behind a download.
- **The new primitive layer is coherent.** Search, filters, menus, switches, dialogs, and code presentation share one restrained interaction vocabulary across light and dark themes.
## Priority Issues
### [P1] Safety context arrives after the install action on mobile
**Why it matters:** Download and copy actions appear in the header, while source capabilities, credential requirements, and installation guidance live in an aside that drops below the long source and discussion sections under the desktop breakpoint. A first-time mobile user can commit before understanding what the widget contacts or what secrets it needs.
**Fix:** Put a compact “Before you install” summary immediately below the header on small screens: API hosts, network scopes, credential count, and action/query counts. Keep the full sticky details panel on desktop.
**Suggested command:** `$impeccable layout`
### [P1] Primary button and own-comment contrast misses AA
**Why it matters:** The requested theme's primary red with white foreground is about 3.4:1, below 4.5:1 for small text. The same pairing affects primary buttons and the author's comment bubbles.
**Fix:** Darken the primary red or use a dark foreground in light mode, then verify default buttons and comment bubbles in both themes.
**Suggested command:** `$impeccable colorize`
### [P2] The listing's server-rendered fallback is blank
**Why it matters:** Before hydration, the listing renders an empty block instead of preserving the page's hierarchy and indicating progress. This creates a visible dead zone on slower self-hosted deployments.
**Fix:** Render the listing heading, toolbar silhouette, and shared skeleton cards as the `BrowserOnly` fallback, matching the detail page's loading treatment.
**Suggested command:** `$impeccable harden`
### [P2] Download, copy, source, and install compete for the same task
**Why it matters:** “Download widget,” “Copy JSON,” a full source viewer, and a separate installation panel all appear as parallel paths. Users must infer the recommended workflow.
**Fix:** Make “Download widget JSON” the single primary header action. Keep copy beside the source viewer and place the next import step directly beside or immediately after download.
**Suggested command:** `$impeccable clarify`
### [P2] Repeated metadata scaffolding creates visual noise
**Why it matters:** Card shells, badges, icon tiles, uppercase tracked labels, and the changelog stripe make the detail page feel assembled from recipes and slow scanning.
**Fix:** Flatten the widget details into a compact definition list, reserve badges for status, and remove the decorative changelog stripe.
**Suggested command:** `$impeccable distill`
## Persona Red Flags
**Jordan, first-timer:** “Network scope,” queries, actions, options, and the manifest schema appear without inline explanation. Download versus copy has no stated recommendation. On mobile, credential and installation context arrives after source and discussion.
**Sam, accessibility-dependent user:** Primary controls and author comment bubbles miss small-text contrast. Copy, optimistic votes, filter result changes, and successful report/edit completion do not expose explicit live-region behavior. Gallery controls and dots are below comfortable touch-target sizes.
**Casey, distracted mobile user:** The most important safety panel falls to the bottom, while primary actions stay at the top. Long source and discussion content separate the decision from its explanation. The submit type selector remains dense at narrow widths.
## Minor Observations
- The internal installation guide uses an external-link icon.
- The final publication action says “Submit” rather than “Publish submission.”
- Dark-theme tokens are declared in two blocks, increasing drift risk.
- Screenshot dots rely on opacity alone and lack `aria-current`.
- Successful report and edit actions deserve a small confirmation instead of silent dismissal.
## Questions to Consider
- If source review is important enough to say “Review exactly what will be installed,” why can users download before seeing the capability and credential summary?
- Is Workshop primarily a code catalog or a safety-conscious app store? The current action hierarchy tries to be both.
- Should the successful end state be a downloaded file, or a confident handoff into Homarr with the next step already visible?
-8
View File
@@ -1,8 +0,0 @@
{
"mcpServers": {
"mantine": {
"command": "npx",
"args": ["-y", "@mantine/mcp-server@9.6.0"]
}
}
}
+1 -6
View File
@@ -10,12 +10,7 @@
"CHANGELOG.md",
"apps/docs/.docusaurus/**",
"apps/docs/build/**",
"apps/docs/blog/2022/**",
"apps/docs/blog/2023/**",
"apps/docs/blog/2024/**",
"apps/docs/blog/2025/**",
"apps/docs/blog/authors.yml",
"apps/docs/static/api/open-api-schema.json",
"apps/docs/blog/**",
"packages/custom-widgets/src/core/component-catalog.generated.json",
"apps/docs/static/custom-widgets/component-catalog-v1.json"
]
-20
View File
@@ -30,26 +30,6 @@
"options": {
"reportUnusedDisableDirectives": "off"
},
"overrides": [
{
"files": ["apps/nextjs/src/**/*.ts", "apps/nextjs/src/**/*.tsx"],
"excludeFiles": ["**/*.spec.ts", "**/*.spec.tsx"],
"rules": {
"eslint/no-restricted-imports": [
"error",
{
"paths": [
{
"name": "@homarr/widgets",
"allowTypeImports": true,
"message": "Import a narrow @homarr/widgets export to preserve widget lazy loading."
}
]
}
]
}
}
],
"ignorePatterns": [
"**/*.config.js",
"**/*.config.mjs",
+8 -30
View File
@@ -7,23 +7,22 @@ homarr/
├── apps/
│ ├── nextjs/ # Main Next.js application (port 3000)
│ ├── docs/ # Docusaurus 3 documentation site (@homarr/docs)
│ ├── tasks/ # Cron-job initialization and scheduling runtime
│ ├── websocket/ # Standalone tRPC WebSocket server (port 3001)
│ └── workshop/ # Go/PocketBase Workshop service
│ ├── tasks/ # Cron job runner + Fastify tRPC API (port 3002)
│ └── websocket/ # Standalone tRPC WebSocket server (port 3001)
├── packages/
│ ├── api/ # tRPC appRouter, procedures, OpenAPI
│ ├── auth/ # NextAuth config, providers, session, API keys
│ ├── db/ # Drizzle schema (3 DB drivers), migrations, queries
│ ├── core/ # Env validation, DB/Redis driver factories, logging
│ ├── definitions/ # Domain enums: WidgetKind, IntegrationKind, permissions
│ ├── widgets/ # Dashboard widget definitions and components
│ ├── widgets/ # All 39 dashboard widgets (definitions + components)
│ ├── integrations/ # Integration classes (HTTP clients to external apps)
│ ├── redis/ # Redis pub/sub channels, caching abstractions
│ ├── translation/ # next-intl setup, locale configs, lang JSON files
│ ├── ui/ # Shared Mantine components, theme, hooks
│ ├── validation/ # Shared zod schemas for API/forms
│ ├── common/ # Shared utilities, IDs, errors
│ ├── cron-jobs/ # Cron job implementations
│ ├── cron-jobs/ # Cron job implementations (25+ jobs)
│ ├── cron-jobs-core/ # Cron scheduling primitives
│ ├── cron-job-status/ # Cron status via Redis
│ ├── boards/ # Board context, edit mode, cache updater
@@ -41,9 +40,8 @@ homarr/
│ ├── analytics/ # Server-side analytics (Umami)
│ ├── server-settings/ # Server setting keys/types
│ ├── settings/ # User-facing settings UI context
│ ├── custom-widgets/ # Custom JSX v2 schema, validation, and runtime
│ ├── onboarding/ # Onboarding studio and setup flow
│ ├── workshop/ # Homarr-side Workshop client and contracts
│ ├── old-import/ # Legacy Homarr import
│ ├── old-schema/ # Legacy Homarr zod schemas
│ └── cli/ # Node CLI for ops (brocli)
├── tooling/
│ ├── typescript/ # Base tsconfig
@@ -59,17 +57,11 @@ homarr/
The documentation site lives at `apps/docs/` (Docusaurus 3, `@homarr/docs`).
Write for smart, curious, advanced users. Documentation should contain only information they cannot reasonably discover or understand directly from the interface: hidden capabilities, non-obvious behavior, prerequisites, constraints, configuration contracts, and migration requirements.
Keep docs concise. Every paragraph must add knowledge beyond visible labels and standard UI conventions. Explain how to discover and use a hidden advanced widget feature; omit explanations that the Delete button deletes an item in edit mode. Avoid UI walkthroughs, exhaustive control descriptions, and repetition. When editing a passage, remove obvious or redundant prose rather than preserving verbosity as a precedent.
Update docs only when a change introduces or alters that non-obvious information, or makes existing guidance inaccurate. Routine fixes and self-explanatory UI changes need no docs update. A new feature alone does not require a page; apply the same reader-value test.
When documentation is needed, use these locations:
When modifying user-facing code, you MUST also update the corresponding documentation:
- New integration → `apps/docs/docs/integrations/<slug>/index.mdx` + `index.ts`
- New widget → `apps/docs/docs/widgets/<slug>/index.mdx` + `index.ts`
- Changed API → `apps/docs/docs/management/api/index.mdx`
- Changed API → `apps/docs/docs/management/api.mdx`
- New/changed env vars → `apps/docs/docs/advanced/`
- New CLI commands → `apps/docs/docs/advanced/command-line/`
- Auth changes → `apps/docs/docs/advanced/` SSO pages
@@ -92,23 +84,9 @@ When documentation is needed, use these locations:
- Lint: oxlint (not ESLint)
- Format: oxfmt (not Prettier)
- UI: Mantine (not Tailwind) — Tailwind is only used in docs app
- Mantine: use the `mantine` MCP server in `.mcp.json` for current v9 APIs before writing component code. Prefer built-in primitives (`Combobox`/`useCombobox`, the polymorphic `component` prop, `@mantine/hooks`) and check `packages/ui/` for existing conventions first.
- Icons: @tabler/icons-react
- Docs app can import from `@homarr/definitions` for shared types
- Run `pnpm dev:cli -- dev` to browse local `homarr:*` images and remote PR images.
- Run `pnpm dev:cli -- build <name>` from a Homarr checkout to build `homarr:<name>` with rebuild provenance.
- Run `pnpm dev:cli -- build --pr <number>` to build a PR locally from a temporary checkout.
- Run Go checks from `tools/homarr-dev` with `go test ./...` and `go vet ./...`.
## MCP servers
`.mcp.json` declares project-scoped MCP servers. Keep only keyless definitions there; credentials belong in local dotfiles. See [Mantine LLM tooling](https://mantine.dev/guides/llms/) for client-specific setup.
## Agent Skills
Portable skills live in `.agents/skills/`. Read the relevant `SKILL.md` before working in that domain; detailed references are loaded only when needed. Claude-compatible discovery is provided through `.claude/skills`.
- `codebase-context` — architecture, package boundaries, and shared utilities
- `documentation-sync` — documentation for changes users need explained
- `mcp-integration` — safe tRPC-to-MCP exposure
- `homarr-custom-widget` — safe Custom JSX v2 authoring
-12
View File
@@ -1,15 +1,3 @@
## [1.77.1](https://github.com/homarr-labs/homarr/compare/v1.77.0...v1.77.1) (2026-09-11)
## [1.77.0](https://github.com/homarr-labs/homarr/compare/v1.76.2...v1.77.0) (2026-09-06)
## [1.76.2](https://github.com/homarr-labs/homarr/compare/v1.76.1...v1.76.2) (2026-08-27)
## [1.76.1](https://github.com/homarr-labs/homarr/compare/v1.76.0...v1.76.1) (2026-08-26)
## [1.76.0](https://github.com/homarr-labs/homarr/compare/v1.75.0...v1.76.0) (2026-08-21)
## [1.75.0](https://github.com/homarr-labs/homarr/compare/v1.74.0...v1.75.0) (2026-08-14)
## [1.74.0](https://github.com/homarr-labs/homarr/compare/v1.73.0...v1.74.0) (2026-08-07)
## [1.73.0](https://github.com/homarr-labs/homarr/compare/v1.72.0...v1.73.0) (2026-07-31)
+96
View File
@@ -0,0 +1,96 @@
# Changes
## Goal
Remove widget-only services from Homarr's global Integration system.
The affected services are release providers and Search.ch. They are only used by specific widgets, so keeping them as global integrations added unnecessary setup, UI, database rows, docs pages, and integration package code.
ArchiveTeam Warrior remains a proper integration but is no longer auto-seeded on first install.
## Why
These services do not need shared integration state.
- Release providers are selected per Releases widget repository.
- Search.ch is only used by the Timetable widget.
Moving them into widget options makes configuration simpler and removes dead-weight integration code from `packages/integrations`.
## What Changed
### Release Providers
- Added release provider definitions in `packages/definitions/src/release-provider.ts`.
- Removed release providers from `IntegrationKind`.
- Moved release-fetching code out of `packages/integrations` and into `packages/request-handler/src/release-providers.ts`.
- Updated the Releases widget to store provider configuration directly in widget options:
- `provider`
- `identifier`
- `versionFilter`
- `providerUrl`
- Updated the Releases widget editor to use a provider select instead of an integration select.
- Updated Docker import mapping so `ghcr.io` maps to `gitHubContainerRegistry`.
- GitHub Container Registry now uses the public OCI registry API and accepts full image references such as `ghcr.io/homarr-labs/homarr:latest`.
### Timetable / Search.ch
- Removed Search.ch as an integration.
- Added a Timetable widget `baseUrl` option, defaulting to `https://search.ch`.
- Moved Search.ch request/parsing logic into `packages/request-handler/src/timetable.ts`.
- Updated the Timetable API to receive `baseUrl` directly from widget options.
### ArchiveTeam Warrior
- Kept as a proper integration (not removed).
- Removed `defaultUrl` from the integration definition to prevent auto-seeding on first install.
### Integration Cleanup
- Removed creators, exports, docs references, and implementation files for the deleted integrations.
- Removed unused dependencies from `packages/integrations/package.json`.
### Database Migration
- Added `packages/db/migrations/custom/0003_remove_widget_only_integrations.ts`.
- The migration moves old integration-linked widget config into widget options, then deletes the old widget-only integration rows.
- Updated the earlier release widget migration to use the new `provider` field instead of `providerIntegrationId`.
### Docs
- Updated Releases widget docs to describe providers instead of integrations.
- Updated Timetable docs to remove Search.ch integration setup.
- Removed obsolete integration docs pages for the deleted integration kinds.
- Updated docs sitemap and integration docs mappings.
### Translations
- Added English labels for the new widget options:
- `widget.timetable.option.baseUrl.label`
- `widget.releases.option.repositories.providerUrl.label`
## How It Works Now
Release provider configuration lives on each Releases widget repository entry. The request handler resolves provider metadata and fetches releases directly from the provider-specific endpoint. GHCR uses anonymous OCI registry access for public packages.
Timetable passes its widget URL option directly to its API router and request handler. No global integration lookup is needed.
ArchiveTeam Warrior continues to use the standard integration system, but users must manually add the integration (it is not auto-created on install).
## Deliberate Non-Goals
- No provider secret/token support was added.
- No new secret storage model was introduced.
- No compatibility layer was kept in `packages/integrations`.
Widget options are client-visible, so storing provider credentials there would be wrong. If authenticated provider requests are needed later, they need a separate secret-storage design.
## Verification So Far
- Focused typechecks passed for affected packages.
- Full `pnpm turbo typecheck` passed.
- Full `pnpm lint` passed with existing warnings.
- Focused release provider normalization tests passed.
- Widget translation test passed.
- Live GHCR smoke checks passed for `ghcr.io/muchobien/pocketbase:latest` and `ghcr.io/homarr-labs/homarr:latest`.
- `pnpm test packages/integrations/test/pi-hole.spec.ts` passed earlier, but failed on rebased reruns because Pi-hole reported `domainsBeingBlocked` as `-2`/`0` instead of `> 1`.
+475
View File
@@ -0,0 +1,475 @@
# Mantine UI - LLM Documentation
This index lists Mantine documentation pages formatted for LLMs.
Each link points to a standalone Markdown file under the /llms path.
For a single consolidated file with all content, use:
- https://mantine.dev/llms-full.txt
## FAQ
- [Are Mantine components accessible?](https://mantine.dev/llms/q-are-mantine-components-accessible.md): Learn about Mantine components accessibility features
- [Browser zooms in when input is focused. What should I do?](https://mantine.dev/llms/q-browser-zooms-on-focus.md): Use meta tag to disable browser scaling or increase input size
- [Can I have color schemes other than light and dark?](https://mantine.dev/llms/q-light-dark-is-not-enough.md): Learn about the difference between color scheme and theme
- [Can I have different primary color for light and dark color schemes?](https://mantine.dev/llms/q-primary-virtual-color.md): Learn how to use virtual color with primary color in theme object
- [Can I remove MultiSelect placeholder when the component has selected values?](https://mantine.dev/llms/q-multiselect-value-placeholder.md): Learn why MultiSelect placeholder is not removed when values are selected and how to remove it with CSS
- [Can I use an array of strings as a list in use-form?](https://mantine.dev/llms/q-list-of-strings-in-use-form.md): Learn about use-form lists limitations
- [Can I use Mantine components as server components?](https://mantine.dev/llms/q-server-components.md): Learn about use client directive and server components usage
- [Can I use Mantine with Astro?](https://mantine.dev/llms/q-can-i-use-mantine-with-astro.md): No, Astro does not support React context
- [Can I use Mantine with Create React App (CRA)?](https://mantine.dev/llms/q-can-i-use-mantine-with-cra.md): Learn how to use Mantine without postcss-preset-mantine or how to eject CRA and customize webpack.config.js and use postcss-preset-mantine
- [Can I use Mantine with Emotion/styled-components/tailwindcss?](https://mantine.dev/llms/q-third-party-styles.md): Learn about limitations of third-party styles
- [Can I use Mantine with Vue/Svelte/Angular/etc.?](https://mantine.dev/llms/q-vue-svelte-angular.md): No, Mantine is a React library and does not support other frameworks/libraries
- [Can I use nested inline styles with Mantine components?](https://mantine.dev/llms/q-nested-inline-styles.md): Nested styles are supported only in CSS files
- [Can I use PostCSS function in inline styles?](https://mantine.dev/llms/q-postcss-fns-inline.md): Learn where PostCSS functions can be used in Mantine
- [Can I use private CSS variables to style components?](https://mantine.dev/llms/q-private-css-variables.md): No, it is not safe and will not work with future versions of Mantine.
- [Can I use SegmentedControl with empty value?](https://mantine.dev/llms/q-segmented-control-no-value.md): SegmentedControl cannot be used without a value
- [How can I add dynamic CSS styles?](https://mantine.dev/llms/q-dynamic-css-styles.md): Use data attributes, CSS variables or inline styles
- [How can I add fuzzy search to Select component?](https://mantine.dev/llms/q-select-fuzzy.md): Learn how to integrate third-party fuzzy search libraries with Mantine Select component
- [How can I add hover styles to an element?](https://mantine.dev/llms/q-how-to-add-hover-styles.md): Learn how to add hover classes to an element with CSS modules, &:hover or @mixin hover
- [How can I apply styles to all Mantine components?](https://mantine.dev/llms/q-apply-styles-to-all.md): Learn how to use attribute selector to apply styles to all Mantine components
- [How can I change body background color?](https://mantine.dev/llms/q-body-background.md): Use CSS to change body background color
- [How can I change component color prop value depending on the color scheme?](https://mantine.dev/llms/q-color-scheme-color.md): Learn how to use CSS variables resolver to change color value depending on the color scheme
- [How can I change input focus styles?](https://mantine.dev/llms/q-input-focus-styles.md): Learn how to use Styles API with Mantine inputs
- [How can I change Tabs border color?](https://mantine.dev/llms/q-tabs-border-color.md): Learn how to use Styles API with Tabs component
- [How can I contribute to the library?](https://mantine.dev/llms/q-how-can-i-contribute.md): There are multiple ways to contribute even without writing code
- [How can I disable all inputs/input groups inside a form?](https://mantine.dev/llms/q-disable-all-inputs-in-form.md): Learn how to disable all inputs/input groups inside a form with the unstyled Fieldset component
- [How can I display different elements in light and dark color schemes?](https://mantine.dev/llms/q-light-dark-elements.md): Learn how to hide/show elements based on color scheme
- [How can I focus the first input with error with use-form?](https://mantine.dev/llms/q-focus-first-input-with-error.md): Learn how to handle focus with use-form hook
- [How can I get current color scheme value in JavaScript?](https://mantine.dev/llms/q-how-to-get-color-scheme-value-in-js.md): How to use useMantineColorScheme and useComputedColorScheme hooks to get current color scheme value in JavaScript
- [How can I lint CSS files?](https://mantine.dev/llms/q-how-to-setup-stylelint.md): Learn how to set up Stylelint for Mantine postcss syntax
- [How can I load fonts in Next.js?](https://mantine.dev/llms/q-next-load-fonts.md): A guide to load custom fonts in Next.js with CSS and next/font package
- [How can I load fonts in React Router?](https://mantine.dev/llms/q-react-router-load-fonts.md): A guide to load custom fonts in React Router
- [How can I load fonts in Vite?](https://mantine.dev/llms/q-vite-load-fonts.md): A guide to load custom fonts in Vite
- [How can I lock scroll in my application?](https://mantine.dev/llms/q-how-to-lock-scroll.md): Use react-remove-scroll library to lock scroll in your application
- [How can I submit a template to Mantine documentation?](https://mantine.dev/llms/q-submit-template.md): Learn how to create and submit a template to Mantine documentation
- [How can I test Modal/Drawer/Popover components?](https://mantine.dev/llms/q-portals-testing.md): Learn how to use react-testing-library to test components that use portals and transitions.
- [How can I test Select/MultiSelect components?](https://mantine.dev/llms/q-combobox-testing.md): Learn how to use react-testing-library to test Select and MultiSelect components.
- [How can I upload files from Dropzone component?](https://mantine.dev/llms/q-dropzone-upload.md): Learn how to process files dropped into Dropzone component
- [How is that thing done on the mantine.dev website?](https://mantine.dev/llms/q-how-that-thing-is-done.md): Learn how various elements are implemented on the mantine.dev website
- [How to align input with a button in a flex container?](https://mantine.dev/llms/q-align-input-button.md): Learn how to align Mantine inputs with buttons using Group component
- [How to call a function when Modal/Drawer closes and animation completes?](https://mantine.dev/llms/q-how-to-call-function-when-modal-closes.md): How to use transitionProps in Modal/Drawer components
- [How to change background color for FiveM?](https://mantine.dev/llms/q-fivem-background-color.md): Unset body background color and color-scheme styles for FiveM
- [How to change input placeholder color?](https://mantine.dev/llms/q-inputs-placeholder-color.md): Learn how to change placeholder color with Styles API
- [How to integrate custom input with use-form hook?](https://mantine.dev/llms/q-custom-input-use-form.md): Learn how to add use-form support for custom inputs
- [How to prevent Modal from closing?](https://mantine.dev/llms/q-how-to-prevent-modal-from-closing.md): Learn how to prevent Modal from closing when the user clicks outside of it or presses the Escape key
- [How to scroll to the top of the form if the form is submitted with errors?](https://mantine.dev/llms/q-scroll-to-the-top-of-the-form.md): Learn how to call a function when the form is submitted with errors
- [How to update Mantine dependencies?](https://mantine.dev/llms/q-how-to-update-dependencies.md): Learn how to update @mantine/_, @mantinex/_ and postcss-preset-mantine dependencies manually or with ncu script
- [How to use Dropzone with @mantine/form?](https://mantine.dev/llms/q-how-to-use-dropzone-with-form.md): Learn how to use Dropzone with @mantine/form to handle file selection state
- [How to use Mantine template on GitHub?](https://mantine.dev/llms/q-templates-usage.md): Learn how to create a new repository based on Mantine template
- [I get hydration warning about data-mantine-color-scheme attribute, what does it mean?](https://mantine.dev/llms/q-color-scheme-hydration-warning.md): Learn how hydration works and how to fix warnings
- [Is there a comparison with other libraries?](https://mantine.dev/llms/q-other-libs.md): Comparison with other libraries is not provided, you are trusted to make your own decision based on documentation examples
- [Is there a floating action button component?](https://mantine.dev/llms/q-floating-action-button.md): No, but you can build it with Affix and ActionIcon components
- [Is there DataGrid component that I can use with Mantine?](https://mantine.dev/llms/q-data-grid-i-need.md): A list of community packages that provide DataGrid component that can be used with Mantine.
- [It is not possible to pinch to zoom when a Modal is opened. What should I do?](https://mantine.dev/llms/q-pinch-to-zoom-modal.md): Use removeScrollProps to configure react-remove-scroll options
- [MantineProvider was not found in component tree. What should I do?](https://mantine.dev/llms/q-mantine-provider-missing.md): Learn how to resolve MantineProvider issues
- [My buttons are transparent and the background is visible only on hover, what is wrong?](https://mantine.dev/llms/q-transparent-buttons.md): You have installed a third-party library that overrides Mantine styles
- [My Popover dropdown closes when I click on the dropdown of a nested Popover](https://mantine.dev/llms/q-nested-popover-closes.md): Popover dropdown is closed when it detects click-outside events
- [My styles are broken with disabled JavaScript. What should I do?](https://mantine.dev/llms/q-disabled-js.md): Learn how to use Mantine without JavaScript enabled
- [My styles are overridden by Mantine components styles, what should I do?](https://mantine.dev/llms/q-styles-order.md): Learn how to use CSS layers to control styles order and prevent Mantine components from overriding your styles
- [Native browser validation does not work in some components, what should I do?](https://mantine.dev/llms/q-native-required.md): Learn why native browser validation does not work in some components
- [The use-local-storage hook returns the real value only after mounting. Is it a bug?](https://mantine.dev/llms/q-local-storage-effect.md): Learn how Mantine retrieves local storage value
- [What is the difference between searchable Select and Autocomplete?](https://mantine.dev/llms/q-select-autocomplete-difference.md): Searchable Select and Autocomplete are similar components, but they serve different purposes.
- [Where can I find the roadmap?](https://mantine.dev/llms/q-roadmap.md): The roadmap is private and only available to the team.
- [Why are my Carousel slides in vertical orientation?](https://mantine.dev/llms/q-carousel-missing-styles.md): You forgot to import carousel styles
- [Why are my notifications displayed at the wrong position?](https://mantine.dev/llms/q-notifications-missing-styles.md): Because you did not import styles
- [Why can I not use one polymorphic component in the component prop of another polymorphic component?](https://mantine.dev/llms/q-polymorphic-in-polymorphic.md): Learn how polymorphic components types work
- [Why can I not use value/label data structure with Autocomplete/TagsInput?](https://mantine.dev/llms/q-autocomplete-value-label.md): Learn the difference between Autocomplete and Select components
- [Why can VSCode not autoimport the Text component?](https://mantine.dev/llms/q-why-vscode-cannot-autoimport-text.md): It is confused by the native Text constructor
- [Why do I see color scheme flickering on page load?](https://mantine.dev/llms/q-color-scheme-flickering.md): Color scheme flickering is caused by incorrect usage of ColorSchemeScript
- [Why do my dates components look broken?](https://mantine.dev/llms/q-dates-missing-styles.md): Because you did not import styles
- [Why does FileButton not work in Menu?](https://mantine.dev/llms/q-file-button-in-menu.md): Learn how to use FileButton in Menu component
- [Why is it required to have 10 shades per color?](https://mantine.dev/llms/q-ten-shades-per-color.md): Learn how Mantine uses theme colors
- [Why is my screen completely empty after I](https://mantine.dev/llms/q-notifications-empty-screen.md): You have used Notifications component incorrectly
## Core
- [Accordion](https://mantine.dev/llms/core-accordion.md): Divide content into collapsible sections
- [ActionIcon](https://mantine.dev/llms/core-action-icon.md): Icon button
- [Affix](https://mantine.dev/llms/core-affix.md): Renders children inside portal at fixed position
- [Alert](https://mantine.dev/llms/core-alert.md): Attract user attention with important static message
- [AlphaSlider](https://mantine.dev/llms/core-alpha-slider.md): Slider component for selecting alpha channel in color pickers (0 – 1)
- [Anchor](https://mantine.dev/llms/core-anchor.md): Display link with theme styles
- [AngleSlider](https://mantine.dev/llms/core-angle-slider.md): Pick angle value between 0 and 360
- [AppShell](https://mantine.dev/llms/core-app-shell.md): Responsive shell for your application with header, navbar, aside and footer
- [AspectRatio](https://mantine.dev/llms/core-aspect-ratio.md): Maintain responsive consistent width/height ratio
- [Autocomplete](https://mantine.dev/llms/core-autocomplete.md): Autocomplete user input with any list of options
- [Avatar](https://mantine.dev/llms/core-avatar.md): Display user profile image, initials or fallback icon
- [BackgroundImage](https://mantine.dev/llms/core-background-image.md): Displays image as background
- [Badge](https://mantine.dev/llms/core-badge.md): Display badge, pill or tag
- [Blockquote](https://mantine.dev/llms/core-blockquote.md): Blockquote with optional cite
- [Box](https://mantine.dev/llms/core-box.md): Base component for all Mantine components
- [Breadcrumbs](https://mantine.dev/llms/core-breadcrumbs.md): Separates list of react nodes with given separator
- [Burger](https://mantine.dev/llms/core-burger.md): Open/close navigation button
- [Button](https://mantine.dev/llms/core-button.md): Button component to render button or link
- [Card](https://mantine.dev/llms/core-card.md): Card with sections
- [Cascader](https://mantine.dev/llms/core-cascader.md): Select a path from hierarchical data with cascading columns
- [Center](https://mantine.dev/llms/core-center.md): Centers content vertically and horizontally
- [Checkbox](https://mantine.dev/llms/core-checkbox.md): Capture boolean input from user
- [Chip](https://mantine.dev/llms/core-chip.md): Pick one or multiple values with inline controls
- [CloseButton](https://mantine.dev/llms/core-close-button.md): Button with close icon
- [Code](https://mantine.dev/llms/core-code.md): Inline and block code
- [Collapse](https://mantine.dev/llms/core-collapse.md): Animate presence with slide down/up transition
- [ColorInput](https://mantine.dev/llms/core-color-input.md): Capture color from user
- [ColorPicker](https://mantine.dev/llms/core-color-picker.md): Pick colors in hex(a), rgb(a), hsl(a) and hsv(a) formats
- [ColorSwatch](https://mantine.dev/llms/core-color-swatch.md): Displays color
- [Combobox](https://mantine.dev/llms/core-combobox.md): Create custom select, autocomplete or multiselect inputs
- [ComboboxPopover](https://mantine.dev/llms/core-combobox-popover.md): Add combobox dropdown to any button element
- [Container](https://mantine.dev/llms/core-container.md): Center content with padding and max-width
- [CopyButton](https://mantine.dev/llms/core-copy-button.md): Copies given text to clipboard
- [CorePackage](https://mantine.dev/llms/core-package.md)
- [DataList](https://mantine.dev/llms/core-data-list.md): Display label-value pairs as a description list
- [Dialog](https://mantine.dev/llms/core-dialog.md): Display a fixed overlay dialog at any side of the screen
- [Divider](https://mantine.dev/llms/core-divider.md): Horizontal line with optional label or vertical divider
- [Drawer](https://mantine.dev/llms/core-drawer.md): Display overlay area at any side of the screen
- [EmptyState](https://mantine.dev/llms/core-empty-state.md): Display a placeholder for no data, empty results and first-run states
- [Fieldset](https://mantine.dev/llms/core-fieldset.md): Group related elements in a form
- [FileButton](https://mantine.dev/llms/core-file-button.md): Open file picker with a button click
- [FileInput](https://mantine.dev/llms/core-file-input.md): Capture files from user
- [Flex](https://mantine.dev/llms/core-flex.md): Compose elements in a flex container
- [FloatingIndicator](https://mantine.dev/llms/core-floating-indicator.md): Display a floating indicator over a group of elements
- [FloatingWindow](https://mantine.dev/llms/core-floating-window.md): Draggable floating area
- [FocusTrap](https://mantine.dev/llms/core-focus-trap.md): Trap focus at child node
- [Grid](https://mantine.dev/llms/core-grid.md): Responsive 12 columns grid system
- [Group](https://mantine.dev/llms/core-group.md): Compose elements and components in a horizontal flex container
- [Highlight](https://mantine.dev/llms/core-highlight.md): Highlight given part of a string with mark
- [HoverCard](https://mantine.dev/llms/core-hover-card.md): Display popover section when target element is hovered
- [HueSlider](https://mantine.dev/llms/core-hue-slider.md): Slider component for selecting hue channel in color pickers (0 – 360 degrees)
- [Image](https://mantine.dev/llms/core-image.md): Image with optional fallback
- [Indicator](https://mantine.dev/llms/core-indicator.md): Display element at the corner of another element
- [Input](https://mantine.dev/llms/core-input.md): Base component to create custom inputs
- [JsonInput](https://mantine.dev/llms/core-json-input.md): Capture json data from user
- [Kbd](https://mantine.dev/llms/core-kbd.md): Display keyboard key
- [List](https://mantine.dev/llms/core-list.md): Display ordered or unordered list
- [Loader](https://mantine.dev/llms/core-loader.md): Indicate loading state
- [LoadingOverlay](https://mantine.dev/llms/core-loading-overlay.md): An overlay with centered loader
- [Mark](https://mantine.dev/llms/core-mark.md): Highlight part of the text
- [Marquee](https://mantine.dev/llms/core-marquee.md): Create continuous scrolling animation for content
- [MaskInput](https://mantine.dev/llms/core-mask-input.md): Input with mask pattern for formatted text entry
- [Menu](https://mantine.dev/llms/core-menu.md): Combine a list of secondary actions into single interactive area
- [Menubar](https://mantine.dev/llms/core-menubar.md): Desktop application style menubar with a row of menu triggers
- [Modal](https://mantine.dev/llms/core-modal.md): An accessible overlay dialog
- [MultiSelect](https://mantine.dev/llms/core-multi-select.md): Custom searchable multi select
- [NativeSelect](https://mantine.dev/llms/core-native-select.md): Native select element based on Input
- [NavLink](https://mantine.dev/llms/core-nav-link.md): Navigation link
- [Notification](https://mantine.dev/llms/core-notification.md): Show dynamic notifications and alerts to user, part of notifications system
- [NumberFormatter](https://mantine.dev/llms/core-number-formatter.md): Format number with thousands/decimal separators and suffix/prefix
- [NumberInput](https://mantine.dev/llms/core-number-input.md): Capture number from user
- [OverflowList](https://mantine.dev/llms/core-overflow-list.md): Hide items that do not fit in a container and display them as a single collapsed item
- [Overlay](https://mantine.dev/llms/core-overlay.md): Overlays parent element with div element with any color and opacity
- [Pagination](https://mantine.dev/llms/core-pagination.md): Display active page and navigate between multiple pages
- [Paper](https://mantine.dev/llms/core-paper.md): Renders white or dark background depending on color scheme
- [PasswordInput](https://mantine.dev/llms/core-password-input.md): Capture password data from user
- [Pill](https://mantine.dev/llms/core-pill.md): Removable and non-removable tags
- [PillsInput](https://mantine.dev/llms/core-pills-input.md): Base component for custom tags inputs and multi selects
- [PinInput](https://mantine.dev/llms/core-pin-input.md): Capture pin code or one time password from the user
- [Popover](https://mantine.dev/llms/core-popover.md): Display popover section relative to given target element
- [Portal](https://mantine.dev/llms/core-portal.md): Renders component outside of parent element tree
- [Progress](https://mantine.dev/llms/core-progress.md): Give user feedback for status of the task
- [Radio](https://mantine.dev/llms/core-radio.md): Wrapper for input type radio
- [RangeSlider](https://mantine.dev/llms/core-range-slider.md): RangeSlider component
- [Rating](https://mantine.dev/llms/core-rating.md): Pick and display rating
- [RingProgress](https://mantine.dev/llms/core-ring-progress.md): Give user feedback for status of the task with circle diagram
- [RollingNumber](https://mantine.dev/llms/core-rolling-number.md): Animate value changes with rolling digits
- [ScrollArea](https://mantine.dev/llms/core-scroll-area.md): Area with custom scrollbars
- [Scroller](https://mantine.dev/llms/core-scroller.md): Horizontal scroll container with navigation controls
- [SegmentedControl](https://mantine.dev/llms/core-segmented-control.md): A linear set of two or more segments
- [Select](https://mantine.dev/llms/core-select.md): Custom searchable select
- [SemiCircleProgress](https://mantine.dev/llms/core-semi-circle-progress.md): Represent progress with semi circle diagram
- [SimpleGrid](https://mantine.dev/llms/core-simple-grid.md): Responsive grid in which each item takes equal amount of space
- [Skeleton](https://mantine.dev/llms/core-skeleton.md): Indicate content loading state
- [Slider](https://mantine.dev/llms/core-slider.md): Slider component
- [Space](https://mantine.dev/llms/core-space.md): Add horizontal or vertical spacing from theme
- [Splitter](https://mantine.dev/llms/core-splitter.md): Resizable split pane layout
- [Spoiler](https://mantine.dev/llms/core-spoiler.md): Hide long sections of content under a spoiler
- [Stack](https://mantine.dev/llms/core-stack.md): Compose elements and components in a vertical flex container
- [Stepper](https://mantine.dev/llms/core-stepper.md): Display content divided into a steps sequence
- [Switch](https://mantine.dev/llms/core-switch.md): Capture boolean input from user
- [Table](https://mantine.dev/llms/core-table.md): Render table with theme styles
- [TableOfContents](https://mantine.dev/llms/core-table-of-contents.md): Renders a list of headings on the page and tracks current heading visible in the viewport
- [Tabs](https://mantine.dev/llms/core-tabs.md): Switch between different views
- [TagsInput](https://mantine.dev/llms/core-tags-input.md): Capture a list of values from user with free input and suggestions
- [Text](https://mantine.dev/llms/core-text.md): Display text
- [Textarea](https://mantine.dev/llms/core-textarea.md): Autosize or regular textarea
- [TextInput](https://mantine.dev/llms/core-text-input.md): Capture string input from user
- [ThemeIcon](https://mantine.dev/llms/core-theme-icon.md): Render icon inside element with theme colors
- [Timeline](https://mantine.dev/llms/core-timeline.md): Display list of events in chronological order
- [Title](https://mantine.dev/llms/core-title.md): h1-h6 heading
- [Tooltip](https://mantine.dev/llms/core-tooltip.md): Renders tooltip at given element on mouse over or other event
- [Transition](https://mantine.dev/llms/core-transition.md): Animate presence of component with pre-made animations
- [Tree](https://mantine.dev/llms/core-tree.md): Display a Tree structure
- [TreeSelect](https://mantine.dev/llms/core-tree-select.md): Select with hierarchical tree data
- [Typography](https://mantine.dev/llms/core-typography.md): Styles provider for html content
- [UnstyledButton](https://mantine.dev/llms/core-unstyled-button.md): Unstyled polymorphic button
- [VisuallyHidden](https://mantine.dev/llms/core-visually-hidden.md): Hide element visually but keep it accessible for screen readers
## Hooks
- [HooksPackage](https://mantine.dev/llms/hooks-package.md)
- [use-element-size](https://mantine.dev/llms/hooks-use-element-size.md): Returns element width and height and observes changes with ResizeObserver
- [useClickOutside](https://mantine.dev/llms/hooks-use-click-outside.md)
- [useClipboard](https://mantine.dev/llms/hooks-use-clipboard.md)
- [useCollapse](https://mantine.dev/llms/hooks-use-collapse.md)
- [useColorScheme](https://mantine.dev/llms/hooks-use-color-scheme.md)
- [useCounter](https://mantine.dev/llms/hooks-use-counter.md)
- [useDebouncedCallback](https://mantine.dev/llms/hooks-use-debounced-callback.md)
- [useDebouncedState](https://mantine.dev/llms/hooks-use-debounced-state.md)
- [useDebouncedValue](https://mantine.dev/llms/hooks-use-debounced-value.md)
- [useDidUpdate](https://mantine.dev/llms/hooks-use-did-update.md)
- [useDisclosure](https://mantine.dev/llms/hooks-use-disclosure.md)
- [useDocumentTitle](https://mantine.dev/llms/hooks-use-document-title.md)
- [useDocumentVisibility](https://mantine.dev/llms/hooks-use-document-visibility.md)
- [useDrag](https://mantine.dev/llms/hooks-use-drag.md)
- [useEventListener](https://mantine.dev/llms/hooks-use-event-listener.md)
- [useEyeDropper](https://mantine.dev/llms/hooks-use-eye-dropper.md)
- [useFavicon](https://mantine.dev/llms/hooks-use-favicon.md)
- [useFetch](https://mantine.dev/llms/hooks-use-fetch.md)
- [useFileDialog](https://mantine.dev/llms/hooks-use-file-dialog.md)
- [useFloatingWindow](https://mantine.dev/llms/hooks-use-floating-window.md)
- [useFocusReturn](https://mantine.dev/llms/hooks-use-focus-return.md)
- [useFocusTrap](https://mantine.dev/llms/hooks-use-focus-trap.md)
- [useFocusWithin](https://mantine.dev/llms/hooks-use-focus-within.md)
- [useForceUpdate](https://mantine.dev/llms/hooks-use-force-update.md)
- [useFullscreen](https://mantine.dev/llms/hooks-use-fullscreen.md)
- [useHash](https://mantine.dev/llms/hooks-use-hash.md)
- [useHeadroom](https://mantine.dev/llms/hooks-use-headroom.md)
- [useHotkeys](https://mantine.dev/llms/hooks-use-hotkeys.md)
- [useHover](https://mantine.dev/llms/hooks-use-hover.md)
- [useId](https://mantine.dev/llms/hooks-use-id.md)
- [useIdle](https://mantine.dev/llms/hooks-use-idle.md)
- [useInputState](https://mantine.dev/llms/hooks-use-input-state.md)
- [useIntersection](https://mantine.dev/llms/hooks-use-intersection.md)
- [useInterval](https://mantine.dev/llms/hooks-use-interval.md)
- [useInViewport](https://mantine.dev/llms/hooks-use-in-viewport.md)
- [useIsFirstRender](https://mantine.dev/llms/hooks-use-is-first-render.md)
- [useIsomorphicEffect](https://mantine.dev/llms/hooks-use-isomorphic-effect.md)
- [useListState](https://mantine.dev/llms/hooks-use-list-state.md)
- [useLocalStorage](https://mantine.dev/llms/hooks-use-local-storage.md)
- [useLogger](https://mantine.dev/llms/hooks-use-logger.md)
- [useLongPress](https://mantine.dev/llms/hooks-use-long-press.md)
- [useMap](https://mantine.dev/llms/hooks-use-map.md)
- [useMask](https://mantine.dev/llms/hooks-use-mask.md)
- [useMediaQuery](https://mantine.dev/llms/hooks-use-media-query.md)
- [useMergedRef](https://mantine.dev/llms/hooks-use-merged-ref.md)
- [useMounted](https://mantine.dev/llms/hooks-use-mounted.md)
- [useMouse](https://mantine.dev/llms/hooks-use-mouse.md)
- [useMove](https://mantine.dev/llms/hooks-use-move.md)
- [useMutationObserver](https://mantine.dev/llms/hooks-use-mutation-observer.md)
- [useNetwork](https://mantine.dev/llms/hooks-use-network.md)
- [useOrientation](https://mantine.dev/llms/hooks-use-orientation.md)
- [useOs](https://mantine.dev/llms/hooks-use-os.md)
- [usePageLeave](https://mantine.dev/llms/hooks-use-page-leave.md)
- [usePagination](https://mantine.dev/llms/hooks-use-pagination.md)
- [usePrevious](https://mantine.dev/llms/hooks-use-previous.md)
- [useQueue](https://mantine.dev/llms/hooks-use-queue.md)
- [useRadialMove](https://mantine.dev/llms/hooks-use-radial-move.md)
- [useReducedMotion](https://mantine.dev/llms/hooks-use-reduced-motion.md)
- [useResizeObserver](https://mantine.dev/llms/hooks-use-resize-observer.md)
- [useRovingIndex](https://mantine.dev/llms/hooks-use-roving-index.md)
- [useScrollDirection](https://mantine.dev/llms/hooks-use-scroll-direction.md)
- [useScroller](https://mantine.dev/llms/hooks-use-scroller.md)
- [useScrollIntoView](https://mantine.dev/llms/hooks-use-scroll-into-view.md)
- [useScrollSpy](https://mantine.dev/llms/hooks-use-scroll-spy.md)
- [useSelection](https://mantine.dev/llms/hooks-use-selection.md)
- [useSet](https://mantine.dev/llms/hooks-use-set.md)
- [useSetState](https://mantine.dev/llms/hooks-use-set-state.md)
- [useShallowEffect](https://mantine.dev/llms/hooks-use-shallow-effect.md)
- [useSplitter](https://mantine.dev/llms/hooks-use-splitter.md)
- [useStateHistory](https://mantine.dev/llms/hooks-use-state-history.md)
- [useTextSelection](https://mantine.dev/llms/hooks-use-text-selection.md)
- [useThrottledCallback](https://mantine.dev/llms/hooks-use-throttled-callback.md)
- [useThrottledState](https://mantine.dev/llms/hooks-use-throttled-state.md)
- [useThrottledValue](https://mantine.dev/llms/hooks-use-throttled-value.md)
- [useTimeout](https://mantine.dev/llms/hooks-use-timeout.md)
- [useToggle](https://mantine.dev/llms/hooks-use-toggle.md)
- [useUncontrolled](https://mantine.dev/llms/hooks-use-uncontrolled.md)
- [useValidatedState](https://mantine.dev/llms/hooks-use-validated-state.md)
- [useViewportSize](https://mantine.dev/llms/hooks-use-viewport-size.md)
- [useWindowEvent](https://mantine.dev/llms/hooks-use-window-event.md)
- [useWindowScroll](https://mantine.dev/llms/hooks-use-window-scroll.md)
## Form
- [Form context](https://mantine.dev/llms/form-create-form-context.md): Add context support to use-form with createFormContext
- [formActions](https://mantine.dev/llms/form-actions.md)
- [formAllInputs](https://mantine.dev/llms/form-all-inputs.md)
- [formErrors](https://mantine.dev/llms/form-errors.md)
- [formGetInputProps](https://mantine.dev/llms/form-get-input-props.md)
- [formNested](https://mantine.dev/llms/form-nested.md)
- [FormPackage](https://mantine.dev/llms/form-package.md)
- [formRecipes](https://mantine.dev/llms/form-recipes.md)
- [formSchemaValidation](https://mantine.dev/llms/form-schema-validation.md)
- [formStatus](https://mantine.dev/llms/form-status.md)
- [formUncontrolled](https://mantine.dev/llms/form-uncontrolled.md)
- [formValidation](https://mantine.dev/llms/form-validation.md)
- [formValidators](https://mantine.dev/llms/form-validators.md)
- [formValues](https://mantine.dev/llms/form-values.md)
- [use-field](https://mantine.dev/llms/form-use-field.md): use-field hook – manage single field state
- [use-form](https://mantine.dev/llms/form-use-form.md): Manage form state
## Dates
- [Calendar](https://mantine.dev/llms/dates-calendar.md): Base component for custom date pickers
- [DateInput](https://mantine.dev/llms/dates-date-input.md): Free form date input
- [DatePicker](https://mantine.dev/llms/dates-date-picker.md): Inline date, multiple dates and dates range picker
- [DatePickerInput](https://mantine.dev/llms/dates-date-picker-input.md): Date, multiple dates and dates range picker input
- [DateTimePicker](https://mantine.dev/llms/dates-date-time-picker.md): Capture datetime from the user
- [GettingStartedDates](https://mantine.dev/llms/dates-getting-started.md)
- [InlineDateTimePicker](https://mantine.dev/llms/dates-inline-date-time-picker.md): Inline date and time picker with range support
- [MiniCalendar](https://mantine.dev/llms/dates-mini-calendar.md): Compact calendar to display a small number of days in a row
- [MonthPicker](https://mantine.dev/llms/dates-month-picker.md): Inline month, multiple months and months range picker
- [MonthPickerInput](https://mantine.dev/llms/dates-month-picker-input.md): Month, multiple months and months range picker input
- [TimeGrid](https://mantine.dev/llms/dates-time-grid.md): Captures time value from the user with a predefined set of options
- [TimeInput](https://mantine.dev/llms/dates-time-input.md): Capture time from the user
- [TimePicker](https://mantine.dev/llms/dates-time-picker.md): Captures time value from the user
- [TimeValue](https://mantine.dev/llms/dates-time-value.md): Display a formatted time value
- [YearPicker](https://mantine.dev/llms/dates-year-picker.md): Inline year, multiple years and years range picker
- [YearPickerInput](https://mantine.dev/llms/dates-year-picker-input.md): Inline year, multiple years and years range picker
## Charts
- [AreaChart](https://mantine.dev/llms/charts-area-chart.md): Area chart component with stacked, percent and split variants
- [BarChart](https://mantine.dev/llms/charts-bar-chart.md): Bar chart component with stacked and percent variants
- [BarsList](https://mantine.dev/llms/charts-bars-list.md): Display a list of bars with names and values
- [BubbleChart](https://mantine.dev/llms/charts-bubble-chart.md): Bubble chart component
- [BulletChart](https://mantine.dev/llms/charts-bullet-chart.md): Compact KPI chart with value, target, and qualitative ranges
- [CompositeChart](https://mantine.dev/llms/charts-composite-chart.md): Composed chart with support for Area, Bar and Line charts
- [DonutChart](https://mantine.dev/llms/charts-donut-chart.md): Donut chart component
- [FunnelChart](https://mantine.dev/llms/charts-funnel-chart.md): Funnel chart component
- [GettingStartedCharts](https://mantine.dev/llms/charts-getting-started.md)
- [Heatmap](https://mantine.dev/llms/charts-heatmap.md): Heatmap chart component
- [LineChart](https://mantine.dev/llms/charts-line-chart.md): Line chart component
- [PieChart](https://mantine.dev/llms/charts-pie-chart.md): Pie chart component
- [RadarChart](https://mantine.dev/llms/charts-radar-chart.md): Radar chart component
- [RadialBarChart](https://mantine.dev/llms/charts-radial-bar-chart.md): Radial bar chart component
- [SankeyChart](https://mantine.dev/llms/charts-sankey-chart.md): Sankey diagram component
- [ScatterChart](https://mantine.dev/llms/charts-scatter-chart.md): Scatter chart component
- [Sparkline](https://mantine.dev/llms/charts-sparkline.md): Simplified area chart to show trends
- [SunburstChart](https://mantine.dev/llms/charts-sunburst-chart.md): Sunburst chart component for hierarchical data
- [Treemap](https://mantine.dev/llms/charts-treemap.md): Treemap chart component
## Schedule
- [AgendaView](https://mantine.dev/llms/schedule-agenda-view.md): Vertical list view of events grouped by date
- [DayView](https://mantine.dev/llms/schedule-day-view.md): Standalone schedule day view component
- [GettingStartedSchedule](https://mantine.dev/llms/schedule-getting-started.md)
- [MobileMonthView](https://mantine.dev/llms/schedule-mobile-month-view.md): Mobile-optimized schedule month view component
- [MonthView](https://mantine.dev/llms/schedule-month-view.md): Standalone schedule month view component
- [RecurringEventsSchedule](https://mantine.dev/llms/schedule-recurring-events.md)
- [ResourcesDayView](https://mantine.dev/llms/schedule-resources-day-view.md): Day view with resource columns for scheduling across resources
- [ResourcesMonthView](https://mantine.dev/llms/schedule-resources-month-view.md): Month view with resource lanes for scheduling across resources
- [ResourcesSchedule](https://mantine.dev/llms/schedule-resources-schedule.md): Schedule wrapper component that combines resource day, week and month views
- [ResourcesWeekView](https://mantine.dev/llms/schedule-resources-week-view.md): Week view with resource rows for scheduling across resources
- [Schedule](https://mantine.dev/llms/schedule-schedule.md): Schedule component with day, week, month and year views
- [ScheduleEventsData](https://mantine.dev/llms/schedule-events-data.md)
- [WeekView](https://mantine.dev/llms/schedule-week-view.md): Standalone schedule week view component
- [YearView](https://mantine.dev/llms/schedule-year-view.md): Standalone schedule year view component
## Guides
- [ControlledVsUncontrolled](https://mantine.dev/llms/guides-controlled-vs-uncontrolled.md)
- [CustomComponents](https://mantine.dev/llms/guides-custom-components.md)
- [EightToNine](https://mantine.dev/llms/guides-8x-to-9x.md)
- [FunctionsReference](https://mantine.dev/llms/guides-functions-reference.md)
- [Gatsby](https://mantine.dev/llms/guides-gatsby.md)
- [Icons](https://mantine.dev/llms/guides-icons.md)
- [JavaScript](https://mantine.dev/llms/guides-javascript.md)
- [Jest](https://mantine.dev/llms/guides-jest.md)
- [LLMDocumentation](https://mantine.dev/llms/guides-llms.md)
- [NextJs](https://mantine.dev/llms/guides-next.md)
- [Polymorphic](https://mantine.dev/llms/guides-polymorphic.md)
- [ReactRouter](https://mantine.dev/llms/guides-react-router.md)
- [Redwood](https://mantine.dev/llms/guides-redwood.md)
- [SevenToEight](https://mantine.dev/llms/guides-7x-to-8x.md)
- [SixToSeven](https://mantine.dev/llms/guides-6x-to-7x.md)
- [Storybook](https://mantine.dev/llms/guides-storybook.md)
- [Tiptap3Migration](https://mantine.dev/llms/guides-tiptap-3-migration.md)
- [TypeScript](https://mantine.dev/llms/guides-typescript.md)
- [Vite](https://mantine.dev/llms/guides-vite.md)
- [Vitest](https://mantine.dev/llms/guides-vitest.md)
## Theming
- [Colors](https://mantine.dev/llms/theming-colors.md)
- [ColorSchemes](https://mantine.dev/llms/theming-color-schemes.md)
- [DefaultProps](https://mantine.dev/llms/theming-default-props.md)
- [MantineProvider](https://mantine.dev/llms/theming-mantine-provider.md)
- [ThemeObject](https://mantine.dev/llms/theming-theme-object.md)
- [Typography](https://mantine.dev/llms/theming-typography.md): Styles provider for html content
## Styles
- [ColorFunctions](https://mantine.dev/llms/styles-color-functions.md)
- [CSSFilesList](https://mantine.dev/llms/styles-css-files-list.md)
- [CSSModules](https://mantine.dev/llms/styles-css-modules.md)
- [CssVariables](https://mantine.dev/llms/styles-css-variables.md)
- [CSSVariablesList](https://mantine.dev/llms/styles-css-variables-list.md)
- [DataAttributes](https://mantine.dev/llms/styles-data-attributes.md)
- [Emotion](https://mantine.dev/llms/styles-emotion.md)
- [GlobalStyles](https://mantine.dev/llms/styles-global-styles.md)
- [MantineStyles](https://mantine.dev/llms/styles-mantine-styles.md)
- [PostCSSPreset](https://mantine.dev/llms/styles-postcss-preset.md)
- [Rem](https://mantine.dev/llms/styles-rem.md)
- [ResponsiveStyles](https://mantine.dev/llms/styles-responsive.md)
- [Rtl](https://mantine.dev/llms/styles-rtl.md)
- [StyleProp](https://mantine.dev/llms/styles-style.md)
- [StyleProps](https://mantine.dev/llms/styles-style-props.md)
- [StylesApi](https://mantine.dev/llms/styles-styles-api.md)
- [StylesOverview](https://mantine.dev/llms/styles-styles-overview.md)
- [StylesPerformance](https://mantine.dev/llms/styles-styles-performance.md)
- [UnstyledComponents](https://mantine.dev/llms/styles-unstyled.md)
- [UsageWithSass](https://mantine.dev/llms/styles-sass.md)
- [VanillaExtract](https://mantine.dev/llms/styles-vanilla-extract.md)
- [VariantsAndSizes](https://mantine.dev/llms/styles-variants-sizes.md)
## X
- [Carousel](https://mantine.dev/llms/x-carousel.md): Embla based carousel component
- [CodeHighlight](https://mantine.dev/llms/x-code-highlight.md): Highlight code with shiki or highlight.js
- [Dropzone](https://mantine.dev/llms/x-dropzone.md): Capture files from user with drag and drop
- [ExtensionsPackage](https://mantine.dev/llms/x-extensions.md)
- [Modals manager](https://mantine.dev/llms/x-modals.md): Centralized modals manager with option to handle state of multi-step modals
- [NavigationProgress](https://mantine.dev/llms/x-nprogress.md): Navigation progress bar
- [Notifications system](https://mantine.dev/llms/x-notifications.md): Mantine notifications system
- [Rich text editor](https://mantine.dev/llms/x-tiptap.md): Tiptap based rich text editor
- [Spotlight](https://mantine.dev/llms/x-spotlight.md): Command center for your application
## Other
- [About](https://mantine.dev/llms/about.md)
- [AllReleases](https://mantine.dev/llms/changelog-all-releases.md)
- [BrowserSupport](https://mantine.dev/llms/browser-support.md)
- [Changelog700](https://mantine.dev/llms/changelog-7-0-0.md)
- [Changelog710](https://mantine.dev/llms/changelog-7-1-0.md)
- [Changelog7100](https://mantine.dev/llms/changelog-7-10-0.md)
- [Changelog7110](https://mantine.dev/llms/changelog-7-11-0.md)
- [Changelog7120](https://mantine.dev/llms/changelog-7-12-0.md)
- [Changelog7130](https://mantine.dev/llms/changelog-7-13-0.md)
- [Changelog7140](https://mantine.dev/llms/changelog-7-14-0.md)
- [Changelog7150](https://mantine.dev/llms/changelog-7-15-0.md)
- [Changelog7160](https://mantine.dev/llms/changelog-7-16-0.md)
- [Changelog7170](https://mantine.dev/llms/changelog-7-17-0.md)
- [Changelog720](https://mantine.dev/llms/changelog-7-2-0.md)
- [Changelog730](https://mantine.dev/llms/changelog-7-3-0.md)
- [Changelog740](https://mantine.dev/llms/changelog-7-4-0.md)
- [Changelog750](https://mantine.dev/llms/changelog-7-5-0.md)
- [Changelog760](https://mantine.dev/llms/changelog-7-6-0.md)
- [Changelog770](https://mantine.dev/llms/changelog-7-7-0.md)
- [Changelog780](https://mantine.dev/llms/changelog-7-8-0.md)
- [Changelog790](https://mantine.dev/llms/changelog-7-9-0.md)
- [Changelog800](https://mantine.dev/llms/changelog-8-0-0.md)
- [Changelog810](https://mantine.dev/llms/changelog-8-1-0.md)
- [Changelog820](https://mantine.dev/llms/changelog-8-2-0.md)
- [Changelog830](https://mantine.dev/llms/changelog-8-3-0.md)
- [Changelog900](https://mantine.dev/llms/changelog-9-0-0.md)
- [Changelog910](https://mantine.dev/llms/changelog-9-1-0.md)
- [Changelog920](https://mantine.dev/llms/changelog-9-2-0.md)
- [Changelog930](https://mantine.dev/llms/changelog-9-3-0.md)
- [Changelog940](https://mantine.dev/llms/changelog-9-4-0.md)
- [Changelog950](https://mantine.dev/llms/changelog-9-5-0.md)
- [Contribute](https://mantine.dev/llms/contribute.md)
- [GettingStarted](https://mantine.dev/llms/getting-started.md)
- [NotFound](https://mantine.dev/llms/404.md)
- [OxcConfig](https://mantine.dev/llms/oxc-config-mantine.md)
- [PreviousChangelogs](https://mantine.dev/llms/changelog-previous-versions.md)
- [Support](https://mantine.dev/llms/support.md)
+39
View File
@@ -0,0 +1,39 @@
# Product
## Register
product
## Users
Homarr is for self-hosters and administrators who assemble dashboards for services across home labs and private infrastructure. They need dense, reliable controls and status at a glance, while retaining enough guidance to configure advanced features without hiding how they work.
## Product Purpose
Homarr provides one place to organize, observe, and operate self-hosted services. Success means users can build useful dashboards quickly, understand their state, recover from configuration errors, act on services, and reshape their workspace without repeatedly leaving the board. Boards must load immediately, stay visually stable, and keep widgets legible across screen sizes.
## Brand Personality
Direct, capable, precise, and approachable. Homarr should feel calm under load, be truthful about system state, and reward users who refine details.
## Anti-references
- Decorative dashboards, oversized metric cards, and motion without state meaning.
- Wizard flows that fight ordinary scrolling or rigid layouts that force one workflow.
- Hidden configuration magic and implementation complexity exposed before it is needed.
- Clever custom controls where familiar platform or Mantine interactions already work.
## Design Principles
1. Keep configuration, consequences, and system state visible.
2. Make simple tasks short without limiting advanced workflows.
3. Preserve expert depth while prioritizing urgent information at small sizes.
4. Reveal dense workspaces progressively through a consistent advanced focus mode.
5. Isolate failures and show loading, stale, partial, empty, and failed states truthfully.
6. Use the existing Mantine design language consistently across management and board surfaces.
7. Keep spatial actions direct: moving, resizing, nesting, and using fixed rails happen on the canvas with immediate feedback.
8. Preserve predictable widget composition with fixed logical tile sizes and horizontal scrolling before controls become inaccessible.
## Accessibility & Inclusion
Target WCAG 2.2 AA. Every pointer interaction needs a keyboard and coarse-pointer path. Support screen readers, visible and restored focus, sufficient contrast, reduced motion, non-color status cues, responsive layouts, actionable error messages, and keyboard equivalents for drag and resize operations.
+263
View File
@@ -0,0 +1,263 @@
# PR #6502 widget-modernization ultra-review
## Executive result
**Merge recommendation: changes required.** No P0 was found, but the current branch still has five P1 issues: a pre-existing Custom API authorization/redirect weakness exposed by this subsystem review, an inaccessible manual advanced-focus interaction, a likely fixed-position/transform geometry defect, systemic false empty states for failed queries, and unvalidated integration-provided navigation URLs. Eight P2 issues cover raw error disclosure, unsafe runtime-ref interfaces, localization, responsive/theming drift, test coverage, and inaccurate PR copy.
Audit health (Impeccable rubric): accessibility 1/4, performance 2/4, responsive design 2/4, theming 2/4, anti-patterns 3/4; **10/20 (Acceptable, significant work required)**. The UI does not look generically AI-generated; its problem is inconsistent technical behavior beneath an otherwise coherent Mantine vocabulary.
## Reviewed revision and environment
| Item | Value |
| ---------------------------- | ---------------------------------------------------------------------------------------------- |
| PR | #6502, `feat: modernize dashboard widgets` |
| Reviewed head | `96e92485a5fd45f3648cfac0453e614779c414ba` |
| Base branch / fetched base | `dev` / `d58a47b776a55d04dee2220f59d072b9af8bbed3` |
| Merge base | `d58a47b776a55d04dee2220f59d072b9af8bbed3` |
| Diff | 359 files, +15,721/-4,478 |
| PR state | open, non-draft, `BLOCKED`; no approval decision |
| Hosted checks at audit start | Fast gate, Container and E2E, CodeQL, JS/TS analysis, Actions analysis, title, auto-fix: green |
| Inline threads | 34 total, 7 unresolved; only 2 remain substantively valid |
| Mantine | repository lock: `@mantine/core`, hooks, charts and dates 9.4.1 |
| Local tools | Node 24.16.0 and pnpm 9.5.0; repository requires Node >=24.18.0 and pnpm >=11.15.1 |
The former red baseline `7b7045202d289b7d83946befcee5d2845ea4a706` had a green Fast gate and a failed Container/E2E run (`30748255760`, job `91498308523`). The latest head has a green Container/E2E job (`30756067198`, job `91519039504`). This proves the current suite passed once; it does not satisfy the requested three-run flake characterization.
## PR-description traceability
| Claim | Result | Evidence / correction |
| --------------------------------------------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| “all 54 widget renderers with compact and advanced modes” | **Inaccurate** | 54 `WidgetKind` values exist. `displayMode="advanced"` reaches 53; `app` is deliberately excluded in `item-content.tsx:51` and documented as compact-only. Change copy to “53 widget renderers; app tiles remain compact,” or implement app advanced mode. |
| advanced focus: hover, keyboard, touch, geometry | **Partial** | Entry paths, Escape ownership, reduced motion, close focus, touch button and zero-dimension guards exist. WID-002/003 remain. |
| richer widget context actions | **Partial** | `Menu.ContextMenu` is a good Mantine choice and query scope is item-aware. Dispatch still crosses `any`; Custom API action authorization is not represented. |
| standardized query identity/cache/status | **Partial** | Query keys, matchers, runtime capture, recovery and tests exist. Thirty-nine of 53 renderer query calls do not read errors; runtime ref registration is render-time mutable state. |
| safe polling | **Mostly confirmed** | defaults are limited to `widget` namespace; Custom API resume immediately refetches; per-widget intervals exist. State/ref typing still needs redesign. |
| partial-failure rendering | **Partial** | settlement helper and several routers are strong; many renderer-level total failures still appear empty/disconnected. |
| bounded integration responses | **Mostly confirmed** | common bounded reader, Custom API 2 MiB, timetable 512 KiB, Traefik bounds and Beszel queue bounds exist. Custom action execution remains outside the hardened fetch path. |
| timetable hardening | **Confirmed** | board/item permission checks, saved-value comparison, public-address resolution, private ranges, per-request DNS re-resolution, pinned dispatcher, redirect rejection, timeouts, response bounds and schemas are present. |
| precise placement | **Mostly confirmed** | mouse, touch tolerance, keyboard, occupied-cell validation, dynamic bounds, nearest fit and immediate persistence exist. Failure/race/live responsive coverage remains insufficient. |
| widget catalog documentation | **Partial** | all kinds have pages and shared interactions; wording overclaims focus behavior and validation counts are stale. |
| validation: 40 files/162 tests, 6/6 E2E | **Stale** | current diff contains 57 changed unit-spec files (including three E2E specs); repository has 42 widget spec files and six E2E specs. Update from the exact final run. |
| independent reviews “pass” | **Not supported** | unresolved valid findings, current P1s, and unavailable live QA contradict an unqualified pass. |
## Mantine suitability ledger
Legend: **U** already used appropriately; **C** consolidation/replacement candidate; **R** relevant but current implementation can stay; **N** not relevant to widget runtime; **D** dependency/version addition. Exact 9.4.1 package archives were inspected in `/tmp`; live docs/Context7 include 9.5.0.
### Core catalog
- **U:** ActionIcon, Alert, Anchor, Avatar, Badge, Box, Button, Card, Center, Code, Divider, Flex, Grid, Group, Image, Loader, Menu (`Menu.ContextMenu`), Overlay, Paper, Popover, Portal, Progress, RingProgress, ScrollArea, SimpleGrid, Skeleton, Stack, Table, Tabs, Text, ThemeIcon, Title, Tooltip, UnstyledButton.
- **C:** Accordion (dense integration sections), DataList (semantic label/value stats), EmptyState (replace bespoke empty state), FocusTrap and Modal compound API (WID-002), LoadingOverlay (background refetch without deleting content), NumberFormatter, OverflowList/Scroller (responsive action and chip rows), Table.ScrollContainer, Timeline (activity/feed histories), VisuallyHidden and Kbd, CloseButton, Dialog/HoverCard/Notification where persistent errors/actions need semantics.
- **R:** Affix, AppShell, AspectRatio, BackgroundImage, Breadcrumbs, Burger, Checkbox, Chip, Collapse, ColorInput/ColorPicker/ColorSwatch/AlphaSlider/AngleSlider/HueSlider, Combobox/ComboboxPopover, Container, CopyButton, Drawer, Fieldset, FileButton/FileInput, FloatingIndicator/FloatingWindow, Highlight, Indicator, Input/JsonInput, List, Mark, Marquee, MaskInput, Menubar, MultiSelect/NativeSelect/NumberInput/PasswordInput/PillsInput/PinInput/Radio/RangeSlider/Rating/SegmentedControl/Select/Slider/Switch/TagsInput/Textarea/TextInput, Pagination, Pill, SemiCircleProgress, Space, Splitter, Spoiler, Stepper, Transition, Tree/TreeSelect, Typography. Use only where an existing widget requirement matches; do not churn working layouts.
- **N:** Alpha/color/angle primitives outside configuration forms, AppShell/navigation primitives, TableOfContents, CorePackage documentation.
- **D (9.5, not 9.4.1):** Cascader. **RollingNumber and Splitter are present in 9.4.1.**
### Hooks catalog
- **C:** `useFocusReturn`, `useFocusTrap`, `useReducedMotion`, `useRovingIndex`, `useLongPress`, `useScroller`, `useElementSize`, `useViewportSize`, `useHotkeys`, `useMediaQuery`, `useResizeObserver`.
- **U/R:** focus-within, hover, disclosure, click-outside, merged-ref, interval/timeout, local-storage, event/window listeners, throttled/debounced values and list/set/map/state helpers are available in 9.4.1 and should replace local mechanics only when behavior and tests improve.
- **N:** document title/favicon/hash/headroom/page-leave/OS/eye-dropper/fullscreen/network/orientation and other page-level utilities have no direct widget-remediation need. Every hook listed under `## Hooks` in `MANTINE.md` is covered by this rule; no hook requires 9.5 for the recommendations above.
### Form, dates, charts, schedule, extensions, theme and styles
- **Form:** existing `@homarr/form` should remain the abstraction over all cataloged form APIs. No widget-runtime replacement; use current validation/status APIs for widget editors (**R**).
- **Dates:** all catalog entries are in installed 9.4.1 dates. `Calendar`, `MiniCalendar`, and `TimeValue` are candidates; other picker/input entries are editor-only (**R**). Importing dates directly into `@homarr/widgets` requires adding that workspace dependency (**D**).
- **Charts:** 9.4.1 exports Area, Bar, BarsList, Bubble, Composite, Donut, Funnel, Heatmap, Line, Pie, Radar, RadialBar, Sankey, Scatter, Sparkline and Treemap (**C/R**). Prefer Sparkline for tiny trends, BarsList for ranked values, Heatmap for calendar/activity density, Composite for mixed analytics. **BulletChart and SunburstChart are 9.5-only (D)**. Replacement requires accessibility and bundle comparison, not blanket Recharts removal.
- **Schedule:** all `AgendaView`, day/week/month/year/resource views and Schedule pages require new `@mantine/schedule` dependency (**D**). AgendaView is the only plausible calendar-widget candidate; its weight and semantics must beat the existing grouped list before adoption.
- **Guides/FAQ/Other:** server components, portal testing, scroll lock, accessibility, nested popovers, zoom, responsive styling and version changelogs are relevant references (**R**); framework/setup/contribution/old migration entries are **N**. The 9.5 changelog is informational only.
- **Theming/styles:** Colors, ColorSchemes, DefaultProps, MantineProvider, ThemeObject, CSS variables, data attributes, CSS Modules, responsive/RTL/style props/Styles API/performance are **U/R**. WID-009 requires shared layer/color tokens. Emotion, Sass, Vanilla Extract and unrelated global setup are **N**.
- **Extensions:** existing modals, notifications, spotlight and rich-text abstractions are **U**; Carousel could consolidate Immich only after behavior parity; Dropzone/CodeHighlight/NProgress are **N** for this audit.
## Complete 54-widget coverage matrix
“Test” means a focused widget-local spec exists, not that all required states are covered. Every row still requires the common contract suite in WID-013.
| WidgetKind | Family | Advanced | Focused test | Principal audit gap |
| --------------------------- | ----------------------- | ----------- | ------------ | ---------------------------------------------------------- |
| clock | basic | yes | no | query error, locale-format options |
| weather | basic/feed | yes | yes | query error; dayjs locale/time patterns |
| app | basic/link | **no** | no | PR wording; URL contract |
| iframe | custom content | yes | yes | manual focus/iframe trapping; URL privacy thread now fixed |
| video | custom content | yes | no | media/focus/live behavior |
| notebook | notes | yes (inner) | yes | editor focus inside manual surface |
| anchorNote | notes | yes | no | query error, save/error regression |
| dnsHoleSummary | network | yes | no | total query error, hard-coded chart colors |
| dnsHoleControls | network/security | yes | no | total error and permission matrix |
| smartHome-entityState | smart home | yes | no | raw state in aria; action permissions |
| smartHome-executeAutomation | smart home | yes | no | mutation/permission/edit-mode E2E |
| stockPrice | analytics | yes | yes | total error |
| mediaServer | media | yes | no | total error and table a11y |
| calendar | calendar | yes | yes | total error; fixed HH:mm; schedule tradeoff |
| downloads | media | yes | yes | four-state/component coverage, arbitrary z-index |
| mediaRequests-requestList | media | yes | no | total error; external URLs/actions |
| mediaRequests-requestStats | media | yes | no | total error; links/colors |
| mediaTranscoding | media | yes | yes | total error and responsive table |
| mediaMissing | media | yes | yes | total error and external URL |
| minecraftServerStatus | infrastructure | yes | no | total error/malformed payload |
| networkControllerSummary | networking | yes | no | total error; WAN/unknown localization |
| networkControllerStatus | networking | yes | no | responsive rule duplication |
| rssFeed | feed | yes | yes | strong stale/partial state; locale relative time |
| bookmarks | basic/link | yes | yes | recomputation; URL contract |
| indexerManager | media | yes | no | total error; runtime action ref |
| healthMonitoring | infrastructure | yes | yes | integration query error |
| releases | infrastructure/feed | yes | yes | secret/item authorization needs E2E |
| mediaReleases | media | yes | no | total error; external URLs |
| dockerContainers | infrastructure | yes | yes | total error; z-index/table semantics |
| firewall | networking/security | yes | yes | partial-query matrix; loose router generics |
| notifications | infrastructure | yes | no | total error; untrusted href |
| systemResources | infrastructure | yes | yes | total error; previous network thread fixed |
| coolify | infrastructure | yes | yes | total error; URL validation |
| systemDisks | infrastructure | yes | yes | total error |
| timetable | calendar | yes | no | backend strong; component locale/live tests absent |
| immich-serverStats | media | yes | yes | two ignored query errors |
| immich-albumCarousel | media | yes | yes (query) | query error and nested overlay/focus E2E |
| paperlessNgx | analytics | yes | no | former error thread fixed, no regression spec |
| patchmon | infrastructure | yes | yes | total error |
| bazarr | media | yes | yes | total error |
| tracearr | analytics | yes | yes | total error |
| speedtestTracker | analytics | yes | yes | total error |
| uptimeKuma | infrastructure | yes | yes | total error |
| audioStats | media | yes | yes | good dual-error handling; partial-state tests needed |
| umami | analytics | yes | no | runtime query registration; chart a11y |
| vpn | networking/security | yes | no | total error; status/permission matrix |
| archiveTeamWarrior | infrastructure | yes | yes | suspense path; docs present |
| ups | infrastructure | yes | no | total error |
| beszelSystemTable | monitoring | yes | yes | nested chart/focus and live subscription |
| beszelSystemGrid | monitoring | yes | no | live/error UI; no focused renderer test |
| beszelAlerts | monitoring | yes | no | partial error and status semantics |
| beszelSystemStats | monitoring | yes | yes | runtime refs, locale tooltip |
| traefik | networking | yes | no | total error; bounded backend positive |
| customApi | custom content/security | yes | yes | WID-001/007/008; polling resume fixed |
## Severity-ranked findings
### WID-001 — P1 confirmed, pre-existing: Custom API actions lack resource authorization and hardened fetch policy
- **Location/claim:** `packages/api/src/router/custom-widget/custom-widget-router.ts:57-102,392-451`; “harden Custom API” and action-permission semantics.
- **Evidence/reproduction:** any authenticated account can call `customWidget.all`, learn every definition ID and URL, then call `customWidget.execute({definitionId})`. The mutation verifies neither board nor item nor an action permission. It decrypts administrator-provided secrets, uses `new URL()` only, and `redirect: "follow"`. The PR hardened `widget.customApi.getData`, but not this sibling execution path.
- **Impact/rule:** unauthorized side effects against internal services and authenticated redirect chains; broken object-level authorization. A view-only account can invoke administrator-created controls outside any board context.
- **Correction:** require `{boardId,itemId,definitionId}`; load the item and verify it is `customApi`, belongs to the board, references the definition, and the caller has an explicit execute policy. Until that policy exists, restrict to admin. Share `validateCustomApiUrl`, timeout, redirect rejection, bounded disposal and redacted errors with getData. Compatibility: old direct callers must send item/board context.
- **Tests:** procedure tests for anonymous, unrelated authenticated, view-only, modifier, admin, mismatched item/definition, disabled definition, redirects, credential URL, timeout and secret non-disclosure; E2E action button across roles.
### WID-002 — P1 confirmed, PR-introduced: manual advanced focus is not an accessible modal
- **Location:** `advanced-focus/context.tsx:194-209`, `items/item-content.tsx:113-189`.
- **Evidence:** manual mode renders an Overlay plus a `role="region"` Card. There is no FocusTrap, `aria-modal`, background inertness, scroll lock or backdrop dismissal. Only the close button is initially focused; Tab can leave the surface and activate dimmed board controls.
- **Impact/rule:** keyboard/screen-reader users lose the interaction boundary; WCAG 2.1.2, 2.4.3 and ARIA dialog pattern. Iframes/editors make escape especially likely.
- **Correction/Mantine:** render manual mode through `Modal.Root` compound parts (or ModalBase), using its portal, FocusTrap, scroll lock, Escape/return-focus and semantic content. Preserve preview mode as non-modal. Configure nested overlays and `removeScrollProps`; keep a 44px `Modal.CloseButton` and explicit title via VisuallyHidden if needed.
- **Tests:** RTL portal/focus-cycle/return tests; Playwright Tab/Shift+Tab, nested Menu/Select/Popover/editor/iframe, backdrop and every close path; reduced motion and 200% zoom.
### WID-003 — P1 probable, PR-introduced: fixed advanced surface remains under transformed Gridstack ancestry
- **Location:** `advanced-focus.module.css:35-42`; Card remains inside the Gridstack item while only the backdrop is portaled.
- **Evidence:** CSS fixed descendants establish their containing block at a transformed ancestor. Gridstack uses transforms for positioned items, so viewport-derived `left/top` can be interpreted relative to the item on affected layouts. A Portal wrapper exists only for Overlay.
- **Impact:** off-screen, clipped or incorrectly scaled focus surface across breakpoints/remounts.
- **Correction:** portal the focused surface itself (naturally achieved by WID-002) and calculate geometry in the same viewport coordinate system. Do not compensate with transform-specific offsets.
- **Tests:** real Gridstack at every layout/viewport, scrolled page, header, dynamic section and resize/remount; assert bounding rect within viewport and finite scale.
### WID-004 — P1 confirmed, regression-exposed/systemic: query failures still become valid empty/disconnected UI
- **Location:** 39 of 53 renderer query calls do not read `error`, including bookmarks, weather, calendar, media server, system resources, Coolify, DNS Hole, Tracearr, notifications, Immich, VPN and others listed in the matrix. Query defaults do not set `throwOnError` (`trpc.tsx:69-93`).
- **Evidence:** TanStack Query returns `data=undefined` on total failure. Components default to `[]`, `null`, or empty branches, so the board ErrorBoundary is never reached. Network Controller and Paperless were fixed, proving the pattern; it was not completed.
- **Impact:** operators are told “empty” or “disconnected” when monitoring failed, violating the product’s truthful-state requirement.
- **Correction/Mantine:** introduce a shared `WidgetQueryState` contract distinguishing initial Skeleton, background LoadingOverlay, stale Alert, partial warning, true EmptyState and terminal error. Either explicitly throw only when no usable cached data or render a shared error Alert. Preserve stale data.
- **Tests:** parameterized contract tests for every queried widget: initial, success-empty, total failure, stale+failure, partial failure, refetch, remount/persisted cache and sibling isolation.
### WID-005 — P1 confirmed, PR-introduced/systemic: integration-provided links bypass a shared URL policy
- **Location:** examples include `media-missing/component.tsx:291`, `notifications/component.tsx:77`, media requests list/stats, Coolify resource/server links, releases, bookmarks and iframe. Only RSS and custom JSX visibly use explicit protocol sanitizers.
- **Evidence:** dynamic strings are assigned directly to `href`/new-tab controls. Integration APIs and remote feeds are trust boundaries; no renderer-wide `http/https` guard is enforced.
- **Impact:** unsafe schemes, credential-bearing URLs, internal detail disclosure and inconsistent opener behavior.
- **Correction:** add one typed application URL helper returning a sanitized URL/undefined, with allowed schemes per use case, credential stripping/rejection, consistent `noopener noreferrer`, and disabled non-link presentation. Validate again server-side for action endpoints.
- **Tests:** `javascript:`, `data:`, malformed, credential, relative, http/https and missing URLs for each shared link primitive.
### WID-006 — P2 confirmed, PR-introduced: runtime query/action state mutates untyped refs during render
- **Location:** `definition.ts:121-162`; runtime registration calls in media missing/transcoding, audio, calendar, Umami, Beszel and Immich; `widget-context-menu.tsx:157-174`.
- **Evidence:** `setWidgetRuntimeQueries` mutates a shared object during render; concurrent/aborted renders can publish uncommitted query identity. Action dispatch casts the definition to `Record<string,unknown>` and `any`, then casts its result back.
- **Correction:** make definition generics preserve kind/options/action types end-to-end; replace stringly opaque ref slots with a typed per-item registration API updated in a committed effect (or use `useSyncExternalStore`). Separate query registration from imperative actions.
- **Tests:** StrictMode, interrupted/remount, options change and cleanup; compile-time `tsd`/type assertions rejecting wrong action props.
### WID-007 — P2 confirmed, regression-exposed: raw backend errors are exposed to users and assistive text
- **Location:** Custom API UI `component.tsx:561-576`; API `custom-api.ts:79-85`; several partial fallbacks use `error.message`.
- **Impact:** internal hostnames, request paths and implementation details can appear in tooltips/aria labels and logs.
- **Correction/Mantine:** return stable error codes plus redacted public messages; keep detailed causes server-side with correlation IDs. Render localized Alert/Notification details, never raw throwable text.
- **Tests:** errors containing credentials, query strings, private hosts and multiline backend bodies must not reach tRPC payload, DOM, accessibility tree or notifications.
### WID-008 — P2 confirmed, PR-introduced/regression-exposed: locale handling remains incomplete
- **Locations:** Beszel chart/tooltip dayjs formats, calendar event `HH:mm`, downloads `YYYY-MM-DD HH:mm`, timetable `HH:mm:ss`, weather day/month/time formats; `_inputs/widget-location-input.tsx:199` hardcodes `Intl.NumberFormat("en")`.
- **Also:** `beszel/_shared/disk-usage.tsx:41`, stats loading aria label, Network Controller `WAN`/`unknown` are English/raw accessible strings.
- **Correction:** centralize app-locale date/time/number/duration/status helpers; load canonical dayjs locale only at the boundary; use localized translation keys for aria/status. NumberFormatter is suitable for simple display but application helpers should own locale.
- **Tests:** at least `en-US`, `fr-FR`, `de-DE`, RTL locale, 12/24-hour preference and long translations, including accessibility names.
### WID-009 — P2 confirmed: layer and color tokens are inconsistent
- **Locations:** downloads z-index 1000 and rgba row colors; item title z-index 4; item menu/dynamic menu 10; media server white rgba; Indexer Manager hex colors; DNS Hole rgba palette; Umami hex tick colors.
- **Correction:** define shared semantic layer tokens relative to Mantine CSS z-index variables and semantic status/chart tokens respecting light/dark/forced colors. Do not replace intentional service branding.
- **Tests:** light/dark/forced-colors with nested overlays and tooltip ordering.
### WID-010 — P2 confirmed: responsive decisions are duplicated and under-tested
- **Location:** Network Controller Status `component.tsx:44,59-78` (open valid thread); Bookmark `component.tsx:94` recomputation (valid but low-impact thread); similar width/height literals across renderers.
- **Correction:** small typed breakpoint lookup helpers per family, memoized only for genuinely expensive layout. Prefer container-query Grid/SimpleGrid where structural. Do not cargo-cult `useMemo` for a cheap pure calculation; benchmark Bookmark before accepting the review suggestion.
- **Tests:** boundary tables for minimum/typical/wide/short/advanced sizes and 200% zoom.
### WID-011 — P2 confirmed: PR/docs overstate behavior and validation
- **Location:** PR body, board/getting-started copy “advanced preview in place,” validation numbers, and unqualified review pass.
- **Correction:** state 53 advanced renderers plus compact-only app; call manual focus an overlay only after WID-002/003; publish exact final-SHA check/test counts and disclose unsupported/live-untested states.
### WID-012 — P2 confirmed: focused automated coverage is far below the 54-widget/data-state claim
- **Evidence:** 42 widget-local spec files are helper-heavy; 20+ renderers have no focused spec. Six E2E files exist, but only board interactions and Custom JSX directly exercise this modernization. The earlier Custom JSX login wait failed once and current exact-route wait remains.
- **Correction:** add shared renderer contract harness plus family E2Es. Replace Custom JSX’s exact post-submit URL race with a shared login helper that waits for authenticated session/stable destination, then run it three times in CI.
### WID-013 — P2 confirmed: open review-thread state no longer matches code
| Thread | Classification |
| ------------------------------------------- | ------------------------------------------------------------------- |
| iframe complete URL | already fixed/false positive at current line: only host is rendered |
| system resources network gate | already fixed, outdated |
| RSS total error | already fixed: no-data error throws; stale data retained |
| bookmarks memoization | valid P3/P2-low; benchmark first |
| network-controller total error | already fixed, outdated |
| network-controller breakpoint consolidation | valid P2 maintainability |
| Paperless total error | already fixed, outdated |
Resolve the five stale threads with evidence; address or explicitly disposition the two live suggestions. Re-run review because CodeRabbit was paused during active development.
## Systemic remediation roadmap
1. **Security/authorization:** WID-001 and WID-005; add negative-role/protocol tests before merge.
2. **Interaction accessibility:** replace manual focus with portaled Mantine modal semantics (WID-002/003), then test nested widget content.
3. **Truthful data state:** build the shared query-state contract and migrate all 39 ambiguous sites; keep valid stale data and partial results.
4. **Typed interfaces:** replace `widgetStateRef` slot mutation and `any` context dispatch with committed typed registrations.
5. **Widget families:** media/downloads, infrastructure, networking/security, smart-home, analytics/calendar/feed, then Beszel; apply URL, state, locale, responsive and a11y contracts in that order.
6. **Design-system cleanup:** semantic layers/colors and restrained Mantine consolidation. Do not add Schedule or upgrade to 9.5 without a separate dependency/bundle decision.
7. **Docs/PR:** correct renderer count, behavior and exact validation evidence.
8. **Final proof:** frozen install on supported Node/pnpm; format, lint, full typecheck, affected and full unit tests, production and docs builds; Custom JSX three times; complete production-container E2E; desktop/tablet/mobile keyboard/touch/light/dark/reduced-motion/zoom/a11y-tree QA. Push nothing until all refer to one unchanged SHA; close every review thread and obtain required approval.
## Positive findings
- Timetable is the strongest security work in the PR: saved resource authorization, private-range rejection, DNS rebinding resistance through pinned lookup, redirect rejection, bounds and schemas are coherently tested.
- Bounded response helpers, Beszel bounded queue/cancellation, and partial-integration settlement materially reduce failure blast radius.
- RSS preserves stale data and distinguishes partial feed failure; Network Controller and Paperless now correctly escalate total failures.
- Item-scoped query matchers and tests address sibling-widget refresh isolation; global polling defaults are restricted to the widget namespace.
- Touch long-press has movement tolerance; focus scale guards prevent Infinity/NaN; reduced-motion and remount cleanup are present.
- Existing Homarr theme, Mantine components and shared UI vocabulary are generally preserved. Most proposed Mantine work is consolidation, not redesign.
## Validation evidence and uncertainty
- `MANTINE.md` was fetched from `https://mantine.dev/llms.txt` and passed byte-for-byte `cmp`; `llms-full.txt` stayed in `/tmp`.
- Context7 current Mantine docs were queried for overlay/focus, responsive/data-state/accessibility, charts and Schedule. Static 9.4.1 archives verified exact exports; Cascader, BulletChart and SunburstChart are 9.5-only.
- Local frozen install could not start because host Node/pnpm are below repository engine requirements. No dependencies, source files or machine toolchain were changed.
- Docker API access is denied, so production-container E2E could not be rerun locally. Hosted exact-head Fast gate and Container/E2E are green.
- `agent-browser` is not installed. Temporary `npx` execution was rejected by the environment’s security reviewer, so live exploratory QA/a11y-tree inspection was not performed.
- External integrations were not available; malformed/partial/permission combinations are static-analysis conclusions unless a focused test already exists.
- This review intentionally changes no production code and makes no commit, push, thread resolution or PR edit.
Finalization must re-query the PR head/checks/threads. If the head differs from the reviewed SHA, this report is a snapshot and the delta requires review before merge.
**Final refresh (2026-08-03):** head remained `96e92485a5fd45f3648cfac0453e614779c414ba`; all reported hosted checks remained successful; merge state remained `BLOCKED` with no approval decision; unresolved threads remained 7 of 34. The branch did not change during the audit.
+1 -2
View File
@@ -108,5 +108,4 @@ pnpm verify:search
## Contributing
When a Homarr change affects user-facing behavior, update the corresponding documentation in this app. See the
[documentation-sync skill](../../.agents/skills/documentation-sync/SKILL.md) for the code-to-documentation mapping.
When making changes to the homarr codebase that affect user-facing behavior, **always update the corresponding documentation in this app**. See `.cursor/rules/documentation-sync.mdc` for the complete mapping of code changes to doc locations.
@@ -13,6 +13,8 @@ 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,11 +10,12 @@ If you are still on a version before 0.15.10, please update to 0.15.10 where we
<!-- truncate -->
## Preserve your old installation
## Export your data
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.
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.
![Export page](./img/old-homarr-export-data.png)
@@ -27,15 +28,28 @@ 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
![Onboarding initial step](./img/onboarding-step-1.png)
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 can upload the zip file you downloaded in the first step.
:::
![Onboarding import dropzone](./img/old-homarr-import-dropzone.png)
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.
![Onboarding import list](./img/old-homarr-import-choose-imported-data.png)
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.
![Onboarding import key input](./img/old-homarr-import-decryption-token.png)
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.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 618 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

@@ -1,159 +0,0 @@
---
authors:
- ajnart
tags: [homarr, update, version, assistant, custom widgets, dashboards, docker]
description: Homarr 2.0 introduces Custom Widgets and Workshop, rebuilt board editing, Assistant tooling, responsive layouts, onboarding, and more.
image: ./img/homarr-v2-dashboard.webp
draft: false
---
# Homarr 2.0.0 - Workshop, Custom Widgets and a rebuilt board
First, thank you.
Many of you used the v2 demo when it was still rough, found bugs and came back to test the next build. Others translated Homarr, answered questions, contributed code or simply kept using it. We would not be shipping this without you.
We are also planning a small batch of **free, limited-edition Homarr swag** for Workshop contributors and a few randomly picked community members. It is not for sale and it is not for profit. No Homarr merch empire, sorry.
Homarr 2.0 builds on the dashboard you already use. Here is what changed.
![A populated Homarr 2.0 dashboard with compact widgets, an Assistant, and an application rail](./img/homarr-v2-dashboard.webp)
<!-- truncate -->
Update your Homarr image as usual. Homarr runs database migrations automatically when the updated container starts; no manual database migration or separate v2 upgrade procedure is needed. Workshop uses the production service by default, with no preview URL configuration required.
## Custom Widgets v2 and Workshop
### Build almost any widget
**This is the big one.** We think it is the most important Homarr update we have made.
Custom Widgets v2 lets you build widgets from JSX, API requests, actions and typed settings. Write one yourself or describe it to Assistant, then test it in the workbench. We think this is enough to make almost any dashboard widget—even that oddly specific component only three people need. Those three people no longer have to wait for us.
This was partly inspired by [Google's upcoming Create My Widget feature](https://blog.google/products-and-platforms/platforms/android/gemini-intelligence/): describe a widget in normal language and let AI make it. Coding knowledge is optional. Knowing what the API does still helps; AI has not fixed bad documentation. Yet.
![The Custom Widget v2 workbench after a live exchange-rate request successfully renders its preview](./img/custom-widget-workbench.webp)
A `homarr-custom-widget-v2` definition can use several fixed-origin API sources. Credentials are encrypted separately and stay out of exports and Workshop submissions. Actions support `GET`, `POST`, `PUT`, `PATCH` and `DELETE`; requests have host checks, timeouts, size limits, permissions and rate limits. Custom Widgets remain **Beta** in 2.0.
### Share widgets and Custom CSS
**Workshop is the other half.** Publish and install Custom Widgets, then update, vote on, comment on and report submissions. Workshop is not only for widgets: you can share your **Custom CSS** there too. Deployment URLs and credentials stay on your Homarr instance.
![The Community Workshop catalog inside Custom Widget management](./img/community-workshop.webp)
:::warning Workshop is moderated
AI makes widgets easy to produce. It does not make the first thing it coughs up worth publishing. **Do not dump untested slop into Workshop.** Test it, add a useful screenshot and explain its setup. Workshop is moderated.
:::
### The ultimate Homarr update
We have called this the **"ultimate Homarr update"**. Not because work stops, but because every new service or widget idea no longer needs a full Homarr release; the community can build and share it instead.
## Board editing, rebuilt
### Drag and drop
We threw out the old drag-and-drop system and rebuilt it on [`dnd-kit`](https://dndkit.com/). Moves and eight-direction resizes are transactional: Homarr previews the result, checks collisions and only saves a valid layout. Bad moves roll back instead of wrecking the board. In short: less fighting with your own dashboard.
![A Calendar tile moving across the rebuilt Homarr 2.0 board grid](./img/drag-and-drop.gif)
### Goodbye Groups and sections, bonjour Containers
Groups and dynamic sections did nearly the same job, so **Containers replace both**. They hold apps, widgets and other Containers. Hold <kbd>Ctrl</kbd> or <kbd>Command</kbd> to move several items at once.
### One board, several layouts
Boards also have Base and Mobile layouts, plus optional breakpoints. **Reset from Base** creates a Mobile starting point without duplicating the board.
## Homarr Assistant
### Work with live Homarr data
Assistant reads live Homarr data and has tools for boards, apps, integrations, widgets, Docker and supported services. It can answer questions, make approved changes and build Custom Widgets. It follows your permissions: reads can run automatically, while changes require approval by default.
![The Homarr Assistant expanded over a live dashboard with health, exploration, media, and board actions](./img/assistant-control-surface.webp)
Open it with <kbd>Shift</kbd> + <kbd>A</kbd>. <kbd>Cmd</kbd>/<kbd>Ctrl</kbd> + <kbd>/</kbd> searches it.
### The Homarr provider
:::note
Assistant ships with a **"Homarr provider"**. It is free, will never offer paid plans, and is paid for by [Ajnart](https://github.com/ajnart) using the ad revenue from this website.
It only uses zero-data-retention (ZDR) providers. [The provider is open source](https://github.com/homarr-labs/homarr/blob/release/v2/apps/workshop/homarr_provider.go), and neither prompts nor responses are logged. We are not selling your data. Pinky promise.
:::
### External assistants through MCP
External assistants can use the same permission-checked tools through `/api/mcp`, with an API key or OAuth 2.1 with PKCE.
## The rest (there is a lot)
### Brand-new onboarding
A six-stage setup studio handles the first admin, core settings, Docker discovery, apps, integrations and the first board.
![The Homarr 2.0 setup studio on its Essentials step, with all six setup stages shown](./img/onboarding-studio.webp)
### Board switcher
Boards now live in a searchable gallery with keyboard navigation. Open it from the header or press <kbd>Shift</kbd> + <kbd>C</kbd>.
![A visual board card inside the searchable Homarr board switcher](./img/board-switcher.webp)
### Command menu and keybinds
<kbd>Cmd</kbd>/<kbd>Ctrl</kbd> + <kbd>K</kbd> finds resources, pages, settings, users, groups and commands. More
keybinds cover Assistant, editing, selection and advanced widget views.
### Customizable header
Pick which buttons appear, put them on the left, center or right, and change their order.
![The Header studio showing left, center, and right zones plus available controls](./img/header-studio.webp)
### Instance branding
Change the name, logo, favicon, colors, corner styles and login page. Global CSS applies instance-wide; board CSS can override it. Only give that power to admins you trust. CSS remains CSS.
![Instance branding controls for application identity, palette, corner style, and global CSS](./img/instance-branding.webp)
### Widgets
Add content without leaving the board. Settings have live previews, and supported widgets have advanced views. Weather and Clock were redesigned; Air Quality, Countdown and Timer are new.
### Docker and Podman
`DOCKER_ENDPOINTS` supports several hosts. Assisted setup and container labels can create apps, integrations, Containers, widgets and health checks.
![Assisted Docker setup listing discovered services that can become apps or integrations](./img/docker-assisted-setup.webp)
Homepage-compatible labels work too.
### Permissions
Groups use a new Viewer, Editor and Admin matrix. The API, Assistant and MCP follow the same rules. LDAP can create users on first sign-in; OIDC can link verified accounts and synchronize groups.
### Performance
Board startup work runs in parallel, heavy screens load when opened, and identical integration requests share work instead of doing it twice.
## Compatibility notes
- Legacy Custom JSX definitions enter a migration state. Homarr keeps encrypted credentials and an archived copy until the v2 definition validates.
- Mobile layouts place rail content in the main flow instead of displaying rails.
- Custom Widgets remain Beta.
- Assistant and MCP do not bypass permissions.
## Links
- Try the [live demo](https://demo.homarr.dev/).
- See [installation and update instructions](/docs/getting-started/installation/docker).
- Browse [Workshop](https://homarr.dev/workshop).
- Read the guides for [Boards](/docs/management/boards), [Custom Widgets](/docs/management/custom-widgets), [Assistant](/docs/management/assistant), [MCP](/docs/management/mcp) and [Docker labels](/docs/integrations/docker-labels).
@@ -1,180 +1,41 @@
# Developer Docker CLI
The host-side developer CLI builds and runs local Homarr images and pull-request previews with non-blocking background operations. Run it directly from a checkout with `pnpm dev:cli -- ...`; no shell-specific setup script is required. It is separate from the recovery CLI included in production images.
The host-side developer CLI builds and runs local Homarr images and pull-request previews. Run it directly from a checkout with `pnpm dev:cli -- ...`; no shell-specific setup script is required. It is separate from the recovery CLI included in production images.
To install an optional `homarr` binary, run `pnpm dev:cli:install` and make your Go bin directory available on `PATH`. GitHub CLI authentication (`gh auth login`) is only needed for pull-request features.
To install an optional `homarr` binary, run `pnpm dev:cli:install` and make your Go bin directory available on `PATH`. GitHub CLI authentication is only needed for pull-request features.
## Requirements
- Docker or OrbStack
- Go 1.25+ (when building the CLI from source)
- GitHub CLI authenticated with `gh auth login` (for pull-request and CI features)
- A [Nerd Font](https://www.nerdfonts.com/) enabled in your terminal (recommended for icons, status indicators, and TUI glyphs)
## Interactive Development Browser
## Browse development images
```bash
pnpm dev:cli --
# or with global binary:
homarr
```
The main browser combines local `homarr:*` images with open pull requests and their GHCR preview status. Select an image to start or stop it, use `R` to build a remote pull request locally in the background or rebuild a local image, or use `p` to force a remote preview pull. Press `d` (or `1`/`2`) to toggle between the image browser and the instance dashboard.
The main screen combines local `homarr:*` images with open pull requests and their GHCR preview status. Select an image to start or stop it, use `R` to build a remote pull request locally in the background or rebuild a local image, or use `p` to force a remote preview pull. Press `d` to switch to the instance dashboard and `d` again to return without losing the browser state. The explicit `dev` and `dash` subcommands start on either screen.
A locally built PR image is folded into its remote PR row only when its recorded revision matches the current GitHub head SHA (marked `◆ local`). When the matching GHCR image is also available, pressing `Enter` opens a local/remote selector with local preselected. Outdated local builds remain separate rows.
A locally built PR image is folded into its remote PR row only when its recorded revision exactly matches the current GitHub head SHA. The row is marked `◆ local`, and starting it prefers that local image without a registry pull. Outdated local builds remain separate rows. Navigation and dashboard switching remain available during background builds.
### Multi-Source Sidebar
Press `Tab` to cycle between sidebar tabs:
- **`[logs]`**: Live container output streams (press `f` to follow/pause, `L` to pin to the selected container, `Page Up`/`Page Down` to scroll).
- **`[build]`**: Real-time BuildKit step streaming for active local builds.
- **`[ci]`**: GitHub Actions CI check statuses, run duration, and workflow links for pull requests.
### Background Task Overlay
Pulls and builds run asynchronously in the background. Press `t` to open the tasks overlay, showing:
- Layer-by-layer download progress and transfer speeds (via Docker Engine API).
- Active build stages and logs.
- Task cancellation and status.
### Browser Shortcuts
| Key | Action |
| ---------------------- | --------------------------------------------------------------------------------------------- |
| `/` | Filter rows by PR number, title, author, branch, CI, image, state, or port |
| `↑` / `↓` or `j` / `k` | Navigate rows |
| `Enter` or `Space` | Start the available image; choose local or remote when both exist; stop if running |
| `R` | Build locally in background (or rebuild local image) |
| `p` | Pull remote PR image in background and start container |
| `m` | Manage mode (`b` rebuild, `i` delete image, `d` delete data, `c` remove container, `p` prune) |
| `t` | Toggle background tasks overlay |
| `Tab` | Cycle sidebar tabs (`[logs]`, `[build]`, `[ci]`) |
| `l` / `L` | Toggle sidebar visibility / Pin logs to selected instance |
| `f` | Follow or pause logs |
| `M` | Toggle demo mode for next launch |
| `b` | Toggle bot pull requests |
| `o` / `a` | Open PR on GitHub / Open running instance in browser |
| `c` | Copy instance URL to clipboard |
| `r` | Refresh data |
| `1` / `2` / `d` | Switch screen (development browser / instance dashboard) |
| `?` / `q` | Help overlay / Quit (cancels active background tasks) |
## Instance Dashboard
## Build images
```bash
homarr dash
# or
pnpm dev:cli -- dash
pnpm dev:cli -- build <name>
pnpm dev:cli -- build --pr <number> [name]
```
The instance dashboard displays all running and stopped Homarr containers, published ports, health states, and resource volumes.
The first command builds the current checkout as `homarr:<name>`. The pull-request form uses a depth-one clone in a temporary directory and defaults to `homarr:pr-<number>` when `name` is omitted.
| Key | Action |
| ------------------ | ---------------------------------------- |
| `Enter` or `Space` | Start or stop selected instance |
| `s` / `S` | Stop / Restart instance |
| `x` | Remove container (preserves data volume) |
| `m` | Manage mode for selected instance |
| `R` | Rebuild from recorded build provenance |
## CLI Commands
The commands below use the installed `homarr` binary. Without global installation, prefix each command with `pnpm dev:cli --` (for example, `pnpm dev:cli -- list`).
### Launch Instances
## Rebuild images
```bash
homarr run dev
homarr run --detach dev
homarr run --pr 6441
homarr run --pr 6441 --demo
homarr --pr 6441 --env WORKSHOP_WEB_URL=https://workshop.example.com/workshop
pnpm dev:cli -- rebuild <name>
```
- `--pr <number>`: Pull and run a PR image from GHCR (shorthand: `homarr --pr <number>`).
- `--demo` (`-m`): Start with mock integrations and sample data.
- `--detach`: Start container in background.
- `--env` (`-e`): Pass additional environment variables (repeatable: `--env KEY=VALUE`).
Images built by this CLI record their source checkout, revision, and optional pull-request number. Rebuild uses that provenance to select the original checkout or create a fresh temporary pull-request checkout.
### Build & Rebuild
## Run images
```bash
# Build current checkout as homarr:<name>
homarr build feature
# Build a pull request locally in a temporary checkout
homarr build --pr 6441
# Rebuild an existing image from recorded provenance
homarr rebuild feature
homarr run <name>
homarr run --pr <number>
```
Images built with the CLI record their source checkout, revision SHA, and optional PR number. `homarr rebuild` reads this provenance to reconstruct the build without manual configuration.
### GitHub CI Checks
```bash
# View CI checks for a specific PR
homarr ci 6441
# View CI checks for the current branch
homarr ci
# Watch CI checks until completion
homarr ci 6441 --watch
```
### Interactive Shell
```bash
# Open interactive shell in running container
homarr shell homarr-dev
# Run specific command in container
homarr shell homarr-dev pnpm db:migration:sqlite:run
```
### Image & Volume Management
```bash
# List local images with build provenance (source, revision, PR)
homarr images
# Delete local image
homarr images rm feature
# List data volumes with owner PR/tag and in-use status
homarr data
# Preview deleting a data volume and its owning container
homarr data rm homarr_feature_data
# Permanently delete the data after reviewing the preview
homarr data rm homarr_feature_data --force
# Remove stopped containers (prompts without --force)
homarr prune --force
```
Forced data deletion removes the derived owner container only when it mounts the exact selected volume. A same-named container that does not own that volume stops the operation.
### Instance Operations & Diagnostics
```bash
# List instances in a script-friendly table
homarr list
# Follow container logs
homarr logs homarr-dev
# Stop, restart, remove, or open container in browser
homarr stop homarr-dev
homarr restart homarr-dev
homarr remove homarr-dev
homarr open homarr-dev
# Verify Docker daemon, Engine API, GHCR access, and gh CLI authentication
homarr doctor
```
Add `--detach` to run in the background, `--demo` to enable demo data, or repeat `--env KEY=VALUE` to provide additional environment variables.
@@ -14,4 +14,4 @@ homarr fix-usernames
This will convert all credential usernames to lowercase and update the database accordingly.
![Terminal output confirming credential usernames were converted to lowercase](./img/screenshot-cli.png)
![](./img/screenshot-cli.png)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 768 KiB

+18 -62
View File
@@ -1,71 +1,27 @@
# Command line interface
The Homarr image includes a recovery CLI for operations that cannot be completed in the web interface. It is not an
application API; use the [API](/docs/management/api) or [MCP endpoint](/docs/management/mcp) for automation.
The Homarr image ships with a built-in recovery tool. This can be useful, if you do not have access to Homarr or if something is not working.
Please note that the CLI is an _emergency tool_, not an API tool.
Back up the database and stop Homarr before running a command that changes data.
Failed commands print an error and return a non-zero exit code, so shell scripts can stop safely.
:::caution
## Run the recovery CLI
Using the recovery CLi tool can cause problems when requests are being sent to Homarr while the tool is running. \
You should also create a backup of the database, in case you break something.
Open a shell in the container, then pass a command to `homarr`:
:::
```sh
docker exec -it <container-id> /bin/bash
homarr <command>
```
## Execute CLI with Docker
### QNAP Container Station
1. Execute `docker ps | grep homarr` on the root system with Docker to find the container ID of your Homarr container.
2. Run `docker exec -it <container-id> /bin/bash homarr <your-command-goes-here>`. \
Replace `<container-id>` with your container ID from step 1. \
Replace `<your-command-goes-here>` with the command you want to execute (see sub-pages).
1. Open **Container Station**.
## Execute CLI with QNAP
![Container Station in the QNAP application menu](./img/334116069-e7ec4726-3df7-4dc6-a3e0-b745e7eef17f.png)
2. Select **Containers**.
![Containers menu in QNAP Container Station](./img/334116205-aa09ce40-0701-4f7d-b9f9-b431b9d207c5.png)
3. Select the Homarr container.
![Homarr container selected in QNAP Container Station](./img/334116303-714b5e07-b9de-4ec0-a2bc-def7ee38750f.png)
4. Select **Execute**.
![Execute action for the Homarr container in QNAP Container Station](./img/334116411-df1482f9-3aa9-4686-b77f-e0dca09e60b2.png)
5. Select `/bin/bash`, then select **Execute**.
![Bash selected in the QNAP execute console](./img/334116518-e1ddb515-99c7-4159-81bf-76e889d1e808.png)
6. Run `homarr <command>` in the console.
![Bash console open for Homarr in QNAP Container Station](./img/334116642-08c4f204-5cd0-4e4e-a785-0142e1b279ca.png)
See the pages in this section for available commands.
## Build a local branch harness
The repository's host-side CLI can build the current checkout and run it as a named Docker harness:
![Homarr developer CLI showing pull requests, local images, and PR CI status](./img/homarr-dev-cli.png)
```sh
pnpm cli harness setup feature-board
```
This creates `homarr:feature-board`, starts it with persistent demo data, and prints the assigned URL. Query the port
again with:
```sh
pnpm cli harness port feature-board
```
Pass environment overrides with repeated `--env` options:
```sh
pnpm cli harness setup feature-board \
--env WORKSHOP_WEB_URL=https://example.test \
--env FEATURE_FLAG=true
```
Related commands are `harness build <name>`, `harness run <name>`, and `harness stop <name>`.
1. Open Container Station in the QNAP Web Interface ![](img/334116069-e7ec4726-3df7-4dc6-a3e0-b745e7eef17f.png)
2. Select Containers in the menu bar on the left ![](img/334116205-aa09ce40-0701-4f7d-b9f9-b431b9d207c5.png)
3. Select your Homarr Container ![](img/334116303-714b5e07-b9de-4ec0-a2bc-def7ee38750f.png)
4. Click "Execute" in the top right of the Window. ![](img/334116411-df1482f9-3aa9-4686-b77f-e0dca09e60b2.png)
5. Select /bin/bash and click Execute ![](img/334116518-e1ddb515-99c7-4159-81bf-76e889d1e808.png)
6. Run the Homarr command and press enter (eg. homarr reset-owner-password) ![](img/334116642-08c4f204-5cd0-4e4e-a785-0142e1b279ca.png)
@@ -10,10 +10,10 @@ Using this command, you can forcefully reset the password of a user.
It will be set to a random value and the hash for the user will be updated.
All sessions for that user will be terminated and the user must re-login.
![Reset-password command help showing that username is required](./img/screenshot-cli.png)
![](./img/screenshot-cli.png)
Using the `--username` parameter, you can specify who you want to reset the password for.
Executing the command will create a new random password, update the database and display the new password.
All sessions for this user will be terminated and they must re-login.
![Terminal output confirming a password reset and session deletion](./img/screenshot-cli-result.png)
![](./img/screenshot-cli-result.png)
@@ -31,13 +31,9 @@ pnpm install --frozen-lockfile
### Run the Homarr application
1. Copy `.env.example` to `.env`.
2. Run `openssl rand -hex 32` twice. Set `AUTH_SECRET` and `SECRET_ENCRYPTION_KEY` to the two different generated
values in `.env`; the example leaves both empty. Keep this file private and preserve `SECRET_ENCRYPTION_KEY`
when reusing a database.
3. Set `DB_URL` to an absolute writable SQLite file path. Migration and application commands run from different
workspace directories, so a relative path can point them at different databases.
4. Keep Redis and the optional local databases running in one terminal.
5. Apply migrations and start Next.js in another terminal.
2. Set `DB_URL` to an absolute writable SQLite file path.
3. Keep Redis and the optional local databases running in one terminal.
4. Apply migrations and start Next.js in another terminal.
```sh
cp .env.example .env
@@ -61,22 +57,8 @@ 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.
`pnpm dev` starts only Next.js. When your work needs scheduled tasks or live subscriptions, run the relevant service
in a separate terminal alongside it:
```sh
pnpm --filter @homarr/tasks dev
```
```sh
pnpm --filter @homarr/websocket dev
```
The WebSocket service listens on port 3001. Production startup embeds both services, so these extra commands are
only needed for development.
Workshop contributors can set up PocketBase and Docusaurus with the dedicated
[Workshop development guide](./workshop).
[Workshop development guide](../../workshop/development).
### Useful commands
@@ -98,27 +80,5 @@ Workshop contributors can set up PocketBase and Docusaurus with the dedicated
- `pnpm typecheck` — typecheck all workspaces.
- `pnpm test` — unit and contract suites.
- `pnpm build` — production monorepo build.
- `docker build -t homarr:local .` — build a production Docker image.
- `docker run -p 7575:7575 -e SECRET_ENCRYPTION_KEY='your_64_character_hex_string' homarr:local` — run it.
## CI and focused validation
The main CI workflow runs lint, typechecking, affected workspace builds, workspace configuration checks, OpenAPI validation, and Custom Widget architecture and bundle checks. Unit and E2E suites do not run in CI. Preview images publish after the Fast gate and both architecture builds succeed; browser tests do not block publication. The container job builds an amd64 image when no preview image was built.
Workshop CI runs only when its workflow or Docker build inputs change. It validates Compose configuration and publishes production images on its configured release branches. Its Docker integration and image tests are manual; workspace typechecks run in the main Fast gate.
Choose the smallest relevant suite when changing behavior:
| Command | Scope | Requirements |
| ------------------------------------------------- | -------------------------------------------------------------- | --------------------------------------------------------------------- |
| `pnpm test <file>` | Focused unit or contract tests | Workspace dependencies |
| `pnpm test` | All unit and contract tests, without Docker integration suites | Workspace dependencies |
| `pnpm test:coverage` | Unit and contract tests with coverage | Workspace dependencies |
| `pnpm test:integration <file>` | Docker-backed service and database compatibility tests | Docker |
| `pnpm test:e2e <file>` | Browser and container scenarios | Docker; Playwright Chromium and system dependencies for browser tests |
| `pnpm test:docs-screenshots` | Regenerate Assistant documentation screenshots | Same browser prerequisites |
| `pnpm test:workshop` / `pnpm test:workshop-image` | Workshop integration / production-image validation | Docker |
Omit `<file>` to run all tests in that command's scope. E2E and screenshot commands use a local image tagged `homarr-e2e`, or an image selected with `HOMARR_E2E_IMAGE`. The default commands run once without automatic retries; coverage is opt-in. The screenshot generator is excluded from both unit and E2E commands.
Keep behavioral coverage for authorization, migrations, persistence, and widget execution. Avoid tests that only search source code for particular component names, JSX, CSS, or documentation wording.
- `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.
@@ -1,102 +0,0 @@
---
title: Widgets and integrations
tags:
- Developer
- Technical documentation
- Widgets
- Integrations
sidebar_position: 2
---
Homarr keeps feature registration explicit and typed. Feature registration has no generator or generated registry.
A new feature is available when its typed metadata and lazy loader are registered.
## Add a widget
Keep the UI, options, helpers, and tests together in `packages/widgets/src/<widget>/`. The usual folder only needs an
`index.ts` definition and a lazily imported `component.tsx`; complex widgets can add local files as needed.
1. Add the stable kind to `packages/definitions/src/widget.ts` and its documentation slug to the direct typed map in
`packages/definitions/src/docs/widget-doc-slugs.ts`.
2. Export `definition` and `componentLoader` from `packages/widgets/src/<widget>/index.ts` with
`createWidgetDefinition`. The local definition owns its icon, query keys, refresh interval, options, matchers, and
errors. Keep the component behind `withDynamicImport` so adding a widget does not load it on every dashboard.
3. For an integration-backed widget, add its integration kinds and selection rules once to
`packages/definitions/src/widget-integration-map.ts`, then spread `getWidgetIntegrationConfig(kind)` into the local
definition. This server-safe config is also the API authorization boundary, avoiding a package cycle without
duplicating widget metadata.
4. Register the widget in both explicit literal loader maps and the type-only module map in
`packages/widgets/src/registry.ts`, then add its icon to `packages/ui/src/widget-icons.ts`. The module and component
loaders must both use literal import paths. Keeping them separate lets a cold widget load request its definition
and component chunks in parallel; the type-only map preserves exact option and component inference. Keep all three
maps in the same order so drift is obvious in review.
5. If the widget needs server data, add its tRPC router under `packages/api/src/router/widgets/` and add its explicit
lazy import to `packages/api/src/router/widgets/index.ts`. Keep upstream requests and Redis response caching in
`@homarr/request-handler`. Use the widget integration middleware for integration-backed procedures.
6. Add canonical documentation in `apps/docs/docs/widgets/<slug>/`.
Weather is the small integration-free example. Downloads shows integration selection, polling, server caching,
mutations, partial upstream failures, and a larger options surface.
## Add an integration
Keep the HTTP client and its capability-specific types in `packages/integrations/src/<integration>/`.
1. Add the typed definition to `packages/definitions/src/integration.ts`, including credential alternatives,
categories, documentation slug, ports, Docker aliases, and onboarding metadata when relevant.
2. Add an explicit lazy creator to `packages/integrations/src/base/creator.ts`. The literal import keeps the
implementation out of the initial server bundle and makes the registration easy to find.
3. Implement only the interfaces required by the widgets that use the service.
4. Add canonical documentation in `apps/docs/docs/integrations/<slug>/`. Its `index.ts` must satisfy
`IntegrationDefinition` so invalid metadata fails type checking without widening the object. Use a type-only import
and `satisfies IntegrationDefinition`; do not cast the object.
Beszel is a compact example with several widgets sharing one integration client.
## Write user documentation
Write for technically capable self-hosters. Keep each page short and operational.
- Start with one sentence describing what the integration or widget adds to Homarr.
- Include prerequisites, credentials, permissions, destructive actions, and non-obvious limits only when they matter.
- Use numbered steps only when order matters. Prefer the exact UI labels a user must select.
- Do not add marketing, implementation history, architecture internals, or explanations of basic self-hosting concepts.
- End when the user can configure and verify the feature; do not repeat the same information in a summary.
The usual page needs only an introduction and a `Configuration` section. Add `Notes` or `Troubleshooting` only for
real caveats.
## Data and errors
- TanStack Query owns browser caching and refresh behavior. Redis owns shared server response caching for bounded key
spaces. Public handlers whose keys come from arbitrary URLs, domains, symbols, or coordinates stay in the bounded
process-local cache.
- Cache raw service responses, never authorization decisions or decrypted credentials. Integration-backed cache keys
include keyed credential fingerprints and the complete integration identity, and are invalidated after mutations.
The fingerprints change with credentials without exposing their values in Redis keys.
- Give upstream requests a finite deadline and pass the request handler's `AbortSignal` into clients that support it.
Admission is bounded per handler. Request deduplication and distributed lock ownership remain active until the
upstream settles or a bounded cleanup grace expires, while unresolved upstream work stays separately capped.
- Keep validation and authorization on the server. Client-side checks improve UX but do not replace tRPC guards.
- Use the shared logger with safe identifiers, operation names, duration, and the original error cause. Never log
secrets or full upstream payloads.
- Multi-integration reads should return successful providers when one service fails and surface a clear error when all
providers fail.
Registration files intentionally contain boring, explicit imports. This small amount of repetition preserves exact
TypeScript inference, gives Next.js discoverable lazy chunks, and keeps Ctrl+click navigation reliable.
## Registry imports
Use `@homarr/widgets/manifest` to load registered widgets. `loadWidgetDefinition` is the metadata-only path;
`loadWidgetComponent` loads only the UI module; and `loadWidgetResources` requests both paths in parallel. These
loaders cache in-flight and successful promises and remove rejected promises so a later request can retry.
The former root `widgetImports` and `loadWidgetDynamic` exports are intentionally not compatibility aliases.
`widgetImports` synchronously imported every widget module, and `loadWidgetDynamic` depended on that eager map.
Restoring them at the package root would defeat lazy definition chunks or add client-only runtime dependencies to
every root consumer. Migrate metadata consumers to `loadWidgetDefinition`, renderers to `loadWidgetResources`, and
type-only registry consumers from the removed internal `WidgetImportRecord` helper to the root `WidgetImports` type.
`@homarr/widgets/catalog` remains as a deprecated compatibility subpath for `widgetCatalogIcons`. New imports should
use `@homarr/ui/widget-icons`, which is the canonical server-safe icon catalog.
@@ -1,67 +0,0 @@
---
title: Workshop operator guide
sidebar_label: Operator guide
sidebar_position: 3
tags:
- Workshop
- Deployment
- Environment variables
- PocketBase
---
# Workshop operator guide
Workshop uses PocketBase. The production image serves the Workshop website and API from one origin and stores state in
`/pb_data`.
## Environment variables
| 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 docs | `HOMARR_WEBSITE_URL` |
| `WORKSHOP_WEB_URL` | Public Workshop URL | `HOMARR_WEBSITE_URL/workshop` |
| `WORKSHOP_PUBLIC_ORIGIN` | PocketBase-generated links and email | Required in production |
| `PB_ALLOWED_ORIGINS` | Allowed browser origins | `*` |
| `GITHUB_CLIENT_ID` | GitHub OAuth application ID | — |
| `GITHUB_CLIENT_SECRET` | GitHub OAuth application secret | — |
| `OPENROUTER_API_KEY` | Enables the Homarr Assistant provider | Provider disabled when empty |
| `HOMARR_AI_DAILY_REQUEST_LIMIT` | Per-user daily request allowance | `50` |
| `HOMARR_AI_GLOBAL_DAILY_REQUEST_LIMIT` | Shared daily request ceiling | `10000` |
| `HOMARR_AI_OPENROUTER_BASE_URL` | OpenRouter-compatible upstream | `https://openrouter.ai/api/v1` |
| `HOMARR_AI_OPENROUTER_MODEL` | Upstream model behind `homarr/model` | Selected by the Homarr team |
Public URL variables accept HTTP(S) origins without embedded credentials, query strings, or fragments. Restart the
container after changing runtime configuration.
## Authentication and email
Configure both GitHub OAuth variables and use `https://<workshop-host>/api/oauth2-redirect` as the callback. Supplying
only one OAuth variable is a startup error. Configure SMTP for comment, report, and removal notifications.
For the central community, `PB_ALLOWED_ORIGINS=*` permits requests from self-hosted Homarr origins. A private deployment
can use a comma-separated allowlist.
## Homarr provider
`OPENROUTER_API_KEY` enables the [Homarr provider](/docs/workshop/homarr-provider). The Workshop server chooses the upstream model and
enforces per-user and global daily limits. It requests zero-data-retention routing, disables data-collecting providers,
and does not retain prompts or responses.
Keep an OpenRouter account-level credit limit as the monetary backstop; request limits are not cost limits.
## Deployment
The v2 image is `ghcr.io/homarr-labs/workshop:v2`; immutable `sha-<commit>` tags are preferable for promotion. The
service listens on port 8090.
When using a reverse proxy, forward the complete hostname, including `/api`, `/_/`, and
`/workshop-runtime-config.js`. Use a dedicated hostname when Homarr already occupies the intended origin.
Verify `/api/health`, the Workshop page, a public listing request, GitHub sign-in, and one installation after deployment.
## Backup and restore
Stop writes and copy `/pb_data` for a consistent backup. A restore replaces the selected PocketBase data directory, so
preserve the current volume and verify that the backup contains `data.db` first. Rehearse the restore outside production
and verify sign-in and one installation afterward.
@@ -1,41 +0,0 @@
---
title: Workshop development
sidebar_position: 4
tags:
- Workshop
- Developer
- Docusaurus
- PocketBase
---
# Workshop development
See the [main development guide](./getting-started) for repository prerequisites.
Start PocketBase:
```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
```
Start Docusaurus in another terminal:
```sh
WORKSHOP_API_URL=http://127.0.0.1:8090 pnpm dev:docs
```
PocketBase runs at `http://127.0.0.1:8090`; create the first superuser under `/_/`. Workshop is available at
`http://127.0.0.1:3003/workshop`.
To connect a local Homarr instance, set:
```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 the affected development process after changing these variables. See
[`apps/workshop/README.md`](https://github.com/homarr-labs/homarr/blob/HEAD/apps/workshop/README.md) for OAuth,
moderation, tests, and service maintenance.
@@ -30,36 +30,15 @@ Using the `PUID` and `PGID` will require you to set the correct permissions on t
| `PUID` | User ID to run the container as | Any valid user ID | `0` |
| `PGID` | Group ID to run the container as | Any valid group ID | `0` |
| `LOG_LEVEL` | Log level to use | `debug` / `info` / `warn` / `error` | `info` |
| `BASE_URL` | Public Homarr origin used by MCP OAuth metadata and redirects. Set this when a reverse proxy does not preserve the public host and protocol. | HTTP(S) origin without a path | - |
| `NO_EXTERNAL_CONNECTION` | Disables Homarr's internet-backed requests, including update checks, weather, location search, and RSS feeds | `true` or `false` | `false` |
| `NO_EXTERNAL_CONNECTION` | Disables Homarr's internet-backed requests, including update checks, weather, and RSS feeds | `true` or `false` | `false` |
| `ENABLE_DNS_CACHING` | Enables dns caching. Enabled by default, but if you experience issues with IPv6 or static IPs, see [#4006](https://github.com/homarr-labs/homarr/issues/4006) or disable this flag | `true` or `false` | `true` |
| `HOMARR_DISABLE_IPV6` | Disables the IPv6 listener of the internal nginx proxy, even when the host supports IPv6. IPv6 is normally disabled automatically when the host has no IPv6 configured. See [#4596](https://github.com/homarr-labs/homarr/issues/4596) | `true` or `false` | `false` |
| `HOMARR_WEBSITE_URL` | Main Homarr documentation/site base used by runtime links. Must be an absolute HTTP(S) URL. | HTTP(S) URL | `https://homarr.dev` |
| `WORKSHOP_API_URL` | PocketBase API used by Homarr and standalone documentation. | HTTP(S) URL | `HOMARR_WEBSITE_URL` |
| `WORKSHOP_WEB_URL` | Public Workshop website used by links in Homarr, Custom Widgets, onboarding, and assistant context. | HTTP(S) URL | `HOMARR_WEBSITE_URL/workshop` |
| `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](/docs/advanced/development/workshop-operator#environment-variables).
### Workshop service
These variables configure the separate Workshop service, not the Homarr application container.
| Environment Variable | Description | Default |
| ------------------------------------------- | ------------------------------------------------------------------------- | --------------------------------- |
| `WORKSHOP_PUBLIC_ORIGIN` | Public origin used in PocketBase links and email | Required in production |
| `PB_ALLOWED_ORIGINS` | Browser origins allowed to call PocketBase | `*` for the public community |
| `WORKSHOP_IMAGE` | Workshop image used by the staging Compose file | `ghcr.io/homarr-labs/workshop:v2` |
| `WORKSHOP_EXPOSE_PORT` | Host port for the combined staging service | `8090` |
| `PB_EXPOSE_PORT` | Host port for local PocketBase development | `8090` |
| `DOCS_EXPOSE_PORT` | Host port for local documentation development | `3003` |
| `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET` | Optional GitHub OAuth application credentials | - |
| `OPENROUTER_API_KEY` | Server-only upstream key that enables the Homarr Assistant provider | Provider disabled |
| `HOMARR_AI_DAILY_REQUEST_LIMIT` | Daily Assistant request units per user | `50` |
| `HOMARR_AI_GLOBAL_DAILY_REQUEST_LIMIT` | Daily Assistant request units shared by all users | `10000` |
| `HOMARR_AI_OPENROUTER_BASE_URL` | OpenRouter-compatible provider endpoint | `https://openrouter.ai/api/v1` |
| `HOMARR_AI_OPENROUTER_MODEL` | Upstream model exposed through the stable `homarr/model` alias | Selected by the Homarr team |
| `HOMARR_AI_ALLOW_INSECURE_UPSTREAM` | Allows an HTTP upstream for local provider tests; never use in production | `false` |
[Workshop operator guide](../../workshop/operator#environment-variables).
## Authentication
@@ -79,12 +58,11 @@ A random secret can be generated by using the following command: `openssl rand -
## Docker
| Environment Variable | Description | Possible values | Default |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ------- |
| `DOCKER_SOCKET_PATHS` | Comma-separated mounted Docker-compatible socket paths. Homarr discovers each path as a selectable Docker environment | For example `/var/run/docker.sock,/var/run/podman.sock` | - |
| `DOCKER_HOSTNAMES` | Comma-separated plaintext TCP hostnames. Pair each hostname with the port in the same position in `DOCKER_PORTS` | For example `localhost,docker.example.com` | - |
| `DOCKER_PORTS` | Comma-separated plaintext TCP ports paired with `DOCKER_HOSTNAMES` | For example `2375,2376` | - |
| `DOCKER_ENDPOINTS` | Optional advanced JSON configuration for custom names, TLS, or restricted capabilities. It takes precedence over the simple variables above. See the [Docker and Podman connection guide](/docs/integrations/docker) | A JSON array of endpoint descriptors | - |
| 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` | - |
## Database
@@ -6,39 +6,17 @@ tags:
Homarr has the following keyboard shortcuts:
| Shortcut | Description |
| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `MOD` + `K` | Opens search, including local apps, boards, integrations, pages, commands, and preferences |
| `MOD` + `P` | Opens app search |
| `MOD` + `/` | Opens Homarr Assistant inside search |
| `MOD` + `Shift` + `/` (`MOD` + `?`) | Opens preference search |
| `Shift` + `A` | Opens the full Homarr Assistant |
| `MOD` + `J` | Toggles Dark Mode |
| `MOD` + `E` | Toggles Edit Mode |
| Hold `Shift` for 500 ms over a supported widget | Opens its temporary advanced view |
| Hold `Shift` + `Ctrl`/`Cmd` over a supported widget | Keeps its advanced view open until it is closed |
| `Esc` | Stops keyboard tile editing, cancels an active pointer or touch move/resize, or closes an open advanced view or board menu |
## Search and commands
Search is designed as the fastest route to Homarr. Start typing after `MOD` + `K` to find local apps, boards,
integrations, pages, commands, and preferences without entering a separate mode first. Local Homarr results appear
before configured search-engine actions and Assistant fallbacks. Commands include opening the board switcher without
leaving the keyboard. Type `!` to enter Web mode when you want to browse configured engine shortcuts or discover
DuckDuckGo bangs; bang discovery is excluded from ordinary Search results.
Use the dedicated shortcuts above to start in apps, Assistant, or preferences. Inside search, choose a mode to narrow
the same query when needed. Media request search is available only when a supported Seerr integration is configured.
## Widget advanced view
Supported widgets open a temporary advanced view when you hold `Shift` over them for 500 ms. Hold `Shift` plus `Ctrl`
on Windows/Linux or `Cmd` on macOS over the widget to keep the advanced view open, or use its menu.
| Shortcut | Description |
| ----------------------------------------------- | ------------------------------------------ |
| `MOD` + `K` | Opens the search bar |
| `MOD` + `J` | Toggles Dark Mode |
| `MOD` + `E` | Toggles Edit Mode |
| Hold `Shift` for 500 ms over a supported widget | Opens its temporary advanced view |
| `Shift` + `Enter` on a supported widget | Opens its advanced view until it is closed |
| `Esc` | Closes an open advanced view or board menu |
## Board editing
`MOD` + `E` enters edit mode. Homarr keeps the current board visible while the editor prepares; movement and resizing become available when it is ready.
When a tile is focused in edit mode:
| Shortcut | Description |
@@ -51,12 +29,13 @@ When a tile is focused in edit mode:
Keyboard moves and resizes apply immediately. To move an item between a canvas, rail, or Container, drag it there or use **Move / resize item** for precise numeric placement.
Commands at a grid boundary or at the tile's minimum size are ignored and announced instead of moving or shrinking the tile.
On touch screens, press and hold a tile before moving it; a quick tap does not start a drag. Resize handles accept direct touch input. During a pointer or touch move or resize, `Esc` cancels the interaction and restores the layout from before it started.
:::tip
Here `MOD` refers to the modifier key, which is `Ctrl` on Windows and `Cmd` on macOS.
Advanced view is available outside edit mode. Widgets without a distinct advanced experience adapt to their normal
dimensions instead. A progress ring follows the pointer while the temporary advanced view is preparing to open.
:::
:::note
+2 -2
View File
@@ -40,7 +40,7 @@ services:
volumes:
- ./homarr/appdata:/appdata
environment:
- BASE_URL=https://your.internal.dns.address.here.com
- BASE_URL=your.internal.dns.address.here.com
- SECRET_ENCRYPTION_KEY=your_64_character_hex_string # <--- can be generated with `openssl rand -hex 32`
networks:
- proxy
@@ -143,7 +143,7 @@ networks:
```
:::important
Homarr and Caddy must share the same Docker network:
Homarr needs to be on the same network so you will need to add the following property to your homarr install:
```yaml
homarr:
@@ -13,8 +13,7 @@ You can change the user and group by using the `PUID` and `PGID` environment var
Important to note is, that you'll also need to change the permissions for the mounted directories accordingly.
:::info
At startup, Homarr updates ownership of its data directories and runtime caches for the selected user and group.
Startup time depends on the amount of persistent data; application code and dependencies do not need ownership changes.
When starting up an updated version of the container it will have about **10 seconds longer to start** as it has to change the permissions of the app directories
:::
## Without mounted Docker socket
@@ -202,9 +202,9 @@ Homarr supports multiple authentication options, from internal userbase (credent
| ``AUTH_OIDC_CLIENT_NAME`` | Display name of provider (in login screen) | OIDC |
| ``AUTH_OIDC_AUTO_LOGIN`` | Automatically redirect to OIDC login | false |
| ``AUTH_OIDC_SCOPE_OVERWRITE`` | Overwrite default scopes (openid, profile, email) | openid email profile groups |
| ``AUTH_OIDC_GROUPS_ATTRIBUTE`` | Attribute used for groups (roles) claim. Dot-separated paths such as `resource_access.homarr.roles` are supported. | groups |
| ``AUTH_OIDC_GROUPS_ATTRIBUTE`` | Attribute used for groups (roles) claim | groups |
| ``AUTH_OIDC_GROUPS_LOCAL_MANAGEMENT`` | Stop syncing group memberships of OIDC users from the groups claim and manage them locally via the group members page instead | false |
| ``AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE`` | Overwrite name attribute. Dot-separated paths are supported. By default it will use preferred_username if it does not contain a `@` and otherwise name. | --- |
| ``AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE`` | Overwrite name attribute. By default it will use preferred_username if it does not contain a `@` and otherwise name. | --- |
| ``AUTH_OIDC_FORCE_USERINFO`` | Force userinfo endpoint to be used for user information. | false |
| ``AUTH_OIDC_ENABLE_DANGEROUS_ACCOUNT_LINKING`` | Enable account linking for OIDC provider. This will link the OIDC accounts by email. Be sure that you have verified emails to prevent stealing accounts. | false |
| ``AUTH_OIDC_ENABLE_DANGEROUS_CREDENTIALS_LINKING`` | Link an OIDC identity to an existing credentials user with the same email. The credentials login and existing user data are preserved. Requires the OIDC profile to contain `email_verified: true`. | false |
@@ -244,21 +244,18 @@ Homarr supports multiple authentication options, from internal userbase (credent
- Create a group (e.g., `homarr-admins`) and assign users who require admin access.
**2. Example .env file:**
Generate `SECRET_ENCRYPTION_KEY` with `openssl rand -hex 32`; do not copy an example key between installs.
```env
OIDC_CLIENT_ID=<REPLACE_WITH_OIDC_CLIENT_ID>
OIDC_CLIENT_SECRET=<REPLACE_WITH_OIDC_CLIENT_SECRET>
OIDC_CLIENT_ID=identificationid #OIDC client ID
OIDC_CLIENT_SECRET=secretsecretsecret #OIDC client secret
OIDC_SLUG=homarr #Application slug in Authentik
AUTH_DOMAIN=auth.example.com #Authentik FQDN
ADMIN_GROUP=homarr-admins #Authentik group for Homarr admins
HOMARR_FQDN=homarr.example.com #Homarr FQDN
SECRET_ENCRYPTION_KEY=<REPLACE_WITH_64_HEX_CHARACTERS>
SECRET_ENCRYPTION_KEY=encryptencryptencrypt #Homarr encryption key
```
**3. Example Docker Compose configuration:**
```yaml {14-24}
```yaml {14-26}
services:
homarr:
image: ghcr.io/homarr-labs/homarr:latest
@@ -272,6 +269,8 @@ Homarr supports multiple authentication options, from internal userbase (credent
environment:
- TZ=America/Los_Angeles
- SECRET_ENCRYPTION_KEY=${SECRET_ENCRYPTION_KEY}
- BASE_URL=https://${HOMARR_FQDN}
- NEXTAUTH_URL=https://${HOMARR_FQDN}
- AUTH_PROVIDERS=oidc #(optional: include ',credentials' to keep local accounts as fallback)
- AUTH_OIDC_CLIENT_ID=${OIDC_CLIENT_ID}
- AUTH_OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET}
@@ -323,18 +322,15 @@ Homarr supports multiple authentication options, from internal userbase (credent
- Record the `Client-ID` and `Client Key` (secret).
**2. Example .env file (Homarr):**
Generate `SECRET_ENCRYPTION_KEY` with `openssl rand -hex 32`; do not copy an example key between installs.
```env
OIDC_CLIENT_ID=<REPLACE_WITH_GOOGLE_CLIENT_ID>
OIDC_CLIENT_SECRET=<REPLACE_WITH_GOOGLE_CLIENT_SECRET>
OIDC_CLIENT_ID=identificationid #Google Client-ID
OIDC_CLIENT_SECRET=secretsecretsecret #Google Client Key (secret)
HOMARR_FQDN=homarr.example.com #Homarr FQDN
SECRET_ENCRYPTION_KEY=<REPLACE_WITH_64_HEX_CHARACTERS>
SECRET_ENCRYPTION_KEY=encryptencryptencrypt #Homarr encryption key
```
**3. Example Docker Compose configuration (Homarr):**
```yaml {14-20}
```yaml {16-22}
services:
homarr:
image: ghcr.io/homarr-labs/homarr:latest
@@ -348,6 +344,8 @@ Homarr supports multiple authentication options, from internal userbase (credent
environment:
- TZ=Europe/Berlin
- SECRET_ENCRYPTION_KEY=${SECRET_ENCRYPTION_KEY}
- BASE_URL=https://${HOMARR_FQDN}
- NEXTAUTH_URL=https://${HOMARR_FQDN}
- AUTH_PROVIDERS=oidc #(optional: include ',credentials' to keep local accounts as fallback)
- AUTH_OIDC_CLIENT_ID=${OIDC_CLIENT_ID}
- AUTH_OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 39 KiB

+85 -20
View File
@@ -1,37 +1,102 @@
# Styling
Homarr uses Mantine and supports custom CSS at two scopes:
:::tip
- **Global:** **Management → Settings → Instance branding → Custom CSS**.
- **Board:** **Board settings → Custom CSS**. Board CSS loads after the global stylesheet and can override it.
Both editors accept direct CSS and Workshop imports.
:::warning
Custom CSS can break after updates and is not an authorization boundary. Hidden controls remain accessible to users who
have permission to use them.
This page is directed at more experienced users with knowledge about CSS and browsers.
We recommend you to [look into the board customizations instead](/docs/management/boards#appearance),
if you do not have experience with writing custom styles or CSS.
:::
## Target elements
:::warning
Use browser developer tools to inspect the element and test a selector. Prefer stable semantic or Mantine class names;
avoid generated classes such as `mantine-Card-x2fske`, which change between builds.
Our support specifically excludes custom CSS, and we will not help you to write CSS.
However, we may help you by adding new CSS classes if required.
To target one widget, add a custom class under **Edit item → Advanced options** and use that class in the board CSS.
:::
Use `!important` only when the existing cascade requires it.
Homarr uses https://mantine.dev/ under the hood. You can change the look & feel of Homarr
using custom CSS, that extends or overwrites the styles from Mantine.
## Theme variables
## Getting into CSS
Mantine exposes [CSS variables](https://mantine.dev/styles/css-variables-list/) that can be overridden globally or per
board:
If you don't have any experience with CSS,
we strongly recommend you to go through the courses here: https://www.w3schools.com/css/.
Due to the infinite scenarios and setups possible with custom CSS,
our support team will not provide any support with your own written CSS.
## Limitations and dangers
When using custom CSS, you can break the page or certain functionalities.
Although CSS can improve the design & feel of the page, it **<ins>cannot do any of the following</ins>**:
- Implement new features, widgets or integrations
- Securely hide elements you don't want users to interact / have permission to
- Completely revamp the design of the page, only smaller changes are possible
Here are some dangers of using custom CSS:
- Updates may contain breaking changes in the structure, built-in styling or how styling is processed. Therefore,
custom CSS may break unexpectedly without any warning
- Custom CSS may make certain elements invisible or not possible to click / interact with
- Custom CSS could make readability and accessibility worse for impaired people
## How to inspect the page
When styling the page, you can use the `Inspector` to inspect elements on the page and try out styles.
This may vary depending on your browser and operating system, but the workflow usually goes like this:
- Open the Homarr page
- Press `Ctrl` + `Shift` + `I` at the same time.
Alternatively, right-click on the page and choose `Inspect` or `Inspect element`.
- Open the tab `Elements` if it isn't open yet.
- Search the element you want to change in the DOM tree or use the element picker (usually at the top left)
to select it by clicking on the page.
- Experiment by changing the CSS code on the right side
When writing custom CSS, it is important to also remember or create a selector, which tells the browser
which element to apply the styles to. If you accidentally broke something or want to revert your changes in the inspector,
simply reload the page.
## Using Homarr's classes
Homarr's elements will already have a lot of classes and styles pre-applied.
You can reuse those classes to apply additional styles or overwrite them.
When overwriting, remember to add the `!important` flag to your CSS property to tell the browser to use our instruction instead.
Classes with random number suffixes, such as `mantine-Card-x2fske`, should be avoided, since those are dynamic
CSS classes that will change with every build. This means, every time you update Homarr,
your styles will no longer apply to them
## Defining custom classes for widgets
If you want to apply the styles only for one widget, you can define custom class names only for that widget.
To do that, enter edit mode on your board, click the three dots of your widget, click `Edit item` and click `Advanced options`.
Then, you can enter as many custom classes as you'd like:
![](./img/widget-custom-classes.png)
To save them, click `Save changes` on both modals and exit edit mode.
## Where to enter custom CSS
After you wrote your custom CSS, it is time to permanently save it, so you don't need to use the inspector anymore.
To do this, enter the board settings:
![](./img/enter-board-settings.png)
Scroll down a bit and click on custom CSS.
Here you can enter your CSS code, which will be applied on the respective board page.
![](./img/custom-css-setting.png)
## Using variables
You can also change the values of the theme variables, which are located here: https://mantine.dev/styles/css-variables-list/.
To do this, see the following example CSS:
```css
:root {
--mantine-primary-color-filled: #ffbb00;
}
```
CSS changes presentation only; use widgets, integrations, permissions, or application code for behavior.
+18 -3
View File
@@ -8,7 +8,22 @@ tags:
# Donate
Donations fund Homarr's development infrastructure and community services.
## Why to donate
Support the project through [OpenCollective](https://opencollective.com/homarr). For sponsorships or donated services,
use the private contact listed on [Get in touch](/docs/community/get-in-touch).
Our team of 4 developers is working on Homarr in their free time.
Homarr has become a big part of our daily things we work on - and therefore we invest a lot of time into maintaining Homarr,
providing support and implementing new features.
We also incur monetary costs while developing Homarr for the tools that we need.
A few examples include:
- Renting for a Cloud VPS for our cloud services (status page, monitoring, email, analytics, [demo instance](https://demo.homarr.dev/), ...)
- Paying for (IDE-) licenses
Using a donation, you help us to cover these expenses and enable the development of Homarr.
Homarr is completely free to use. We'd highly appreciate if you consider donating, especially if you use Homarr in a commercial environment.
## Where to donate
We accept donations on our official [OpenCollective](https://opencollective.com/homarr).
You may also sponsor us directly by providing a useful service / product to us other means.
You can get into contact with us [here](/docs/community/get-in-touch).
+87 -28
View File
@@ -1,51 +1,110 @@
---
title: Frequently Asked Questions
description: Answers to common Homarr questions about installation, customization, connectivity, support, and commercial use.
tags:
- Community
- Help
---
## Can Homarr run on a Raspberry Pi?
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
Yes, when the device uses a supported `linux/arm64` operating system. `linux/arm/v7` is not supported. See
[Prerequisites](/docs/getting-started).
### Can I install Homarr on a Raspberry Pi?
## Can I use custom app icons?
Yes, Homarr is compatible with the architecture of the Raspberry Pi. Have a look in our <a href="/docs/category/installation-1">installation guide</a> on how to do so.
Yes. Use a direct image URL, one of Homarr's icon sources, or upload an image. See [Icons](/docs/advanced/icons).
### I have a suggestion / idea for improvement or want to report a bug
## Can I create widgets?
You can report issues, suggestions, or ideas for improvements <a href="https://github.com/homarr-labs/homarr/issues/new/choose">here</a>.
Yes. [Custom Widgets](/docs/management/custom-widgets) use server-side API requests and safe JSX. They can be written in
the workbench or created with an MCP-connected agent.
### Can I use my own custom icons for apps?
## Can Homarr be exposed to the internet?
Yes, check out our guide on <a href="/docs/advanced/customizations/icons">how to add your custom icons</a>.
Homarr supports authenticated and public deployments. Terminate HTTPS at a reverse proxy, keep Homarr updated, and grant
public board access only when anonymous viewing is intended. See [Board access control](/docs/management/boards#access-control)
and [Single sign-on](/docs/advanced/single-sign-on).
### Some of my online icons are not working (anymore)
## Where are the logs?
It seems the `cdn.jsdelivr.net` database of icon sometimes appear in the lists for DNS-Holes (PiHole, AdGuard Home...). <br />
So make sure to manually whitelist this address.
Use **Management → Tools → Logs** for retained and live application logs. Container startup failures and older events are
available through the deployment platform, for example `docker container logs homarr`.
### Can I add my own widgets? Do I need programming skills?
Use the browser developer console only for client-side failures. Review logs before sharing them because URLs and other
deployment details may be present.
Yes. A [Custom Widget](/docs/management/custom-widgets) can read a public or private HTTP API.
## Why can Homarr not reach a service behind Gluetun?
It can show responsive Mantine JSX. It can also use widget options and actions with board permission checks.
Gluetun's firewall blocks traffic that is not explicitly allowed. Configure its
[`FIREWALL_OUTBOUND_SUBNETS`](https://github.com/qdm12/gluetun-wiki/blob/main/setup/options/firewall.md) for the relevant
LAN or Docker subnet, and use an address reachable from the Homarr container.
Custom JSX v2 is the only Custom Widget format. Write the JSON and JSX in the workbench.
## Where do I report a bug or request a feature?
You can also use the AI copy prompt. An MCP agent can validate and preview a definition before it saves the definition.
Use the [GitHub issue templates](https://github.com/homarr-labs/homarr/issues/new/choose). For community support, see
[Get in touch](/docs/community/get-in-touch).
The workbench reads a pasted AI response. It shows request capabilities and redacted diagnostics.
## Can Homarr be used commercially?
A template cannot import packages or run arbitrary JavaScript. It cannot call browser `fetch`.
Yes. Homarr is licensed under the [Apache License 2.0](/docs/community/license). Commercial users can support development
through [OpenCollective](/docs/community/donate).
Named requests use the Homarr server. The server applies board permissions and the network policy.
Before installation, examine the origins, methods, permissions, and actions. Use the same review before publication.
### Is it secure to expose Homarr to the web?
Yes, since v0.14.0, we are confident that Homarr is secure enough to be exposed to the web.
Make sure to enable the "allow anonymous access" [option in the settings](/docs/management/boards#access-control), if you want to allow people without an account to be able to see your dashboard (not edit)
### How do I open the console / log?
There are two different sources of information:
- console: This is your client (browser). It can tell us, if there is an issue on the client (crash, bug, ...).
- log: This is your server. It will tell us, if there are issues with connections for the integrations, crashes and errors.
Both variants _can_ contain personal data. It's recommended, that you don't post them publicly or remove personal data from it.
<Tabs>
<TabItem value="apple" label="Read console" default>
> Note: This will not work on most mobile browsers.
Open your browser and ensure, that Homarr is your current tab.
Press `CTRL` + `SHIFT` + `I` on your keyboard. If this shortcut doesn't work, please use one of the alternatives: https://developer.chrome.com/docs/devtools/open/.
Click on `Console` in the new window, that opened. There, you should see white, orange and sometimes red messages. You can use the Windows Snip & Sketch tool (or your respective tool on Linux / MacOS) to screenshot those messages.
> Please never execute any commands in the console. Scammers are using the console to let you execute harmful commands. If you want to resolve an issue in Homarr, **you'll never have to execute any commands here**
</TabItem>
<TabItem value="orange" label="Read log">
This depends on your installation method. If you use Unraid, you can simply click on the App and on `Log`. If you have a bare Docker install, use `docker container logs homarr`. If you used a different name than `homarr` for your container, you need to replace said argument. Here is some additional information about the command: https://geekflare.com/check-docker-logs/.
</TabItem>
</Tabs>
### How do I post logs?
Please check [the section above](#how-do-i-open-the-console--log), on how to obtain logs. Logs should always be posted to https://pastebin.com/ or https://rentry.co/.
### Can I use Homarr at my company?
Yes, you can! But please consider a donation, if our work has been useful to you.
### I think I found a bug. What can I do?
If you think, that you found a bug, please create an issue <a href="https://github.com/homarr-labs/homarr/issues/new/choose">here</a>.
### Why can't I use my custom icons?
You need to restart the Homarr Docker container after adding icons.
### Why can't homarr see/ping containers running locally behind GluetunVPN?
This is because GluetunVPN, and many other docker VPN services, secure their VPN connections with strict firewalls. These firewalls often block access to local containers, even on the same docker network. The workaround for <a href="https://github.com/qdm12/gluetun-wiki/blob/main/setup/options/firewall.md">GluetunVPN</a> is to add "- FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24" into the environmental variables of the docker compose file. If you are using a firewall on your system, you will need to add a rule that allows requests from ip's in the range of 192.168.xxx.xxx . If you are using UFW the command would be "sudo ufw allow from 192.168.0.0/16".
Tip: If you are running your torrent client behind a VPN, and running Sonarr (or something similar) normally, make sure to open your torrent client's port in your firewall. If you don't, Sonarr will not be able to see it. Furthermore, use your machines IP (192.168.x.x) rather then 127.0.0.1 or localhost.
### Can I get in touch with you?
Yes, go <a href="/docs/community/get-in-touch">here</a> to get with us in touch.
<hr />
:::tip
Haven't found the solution to your problem / question? We are eager to help you on [our Discord](/docs/community/get-in-touch#discord) or in a [GitHub Issue](https://github.com/homarr-labs/homarr/issues/new/choose).
:::
+12 -11
View File
@@ -1,23 +1,24 @@
---
title: Get in touch
description: Community, support, and private contact channels.
title: Get In Touch
description: Still have a question? Click here!
tags:
- Help
- Community
---
# Get in touch
We're happy to talk with you! Shoot us a message at the following places:
## GitHub
## E-Mail
Use the [issue templates](https://github.com/homarr-labs/homarr/issues/new/choose) for bugs and feature requests.
We are open to discuss sponsorships, security vulnerabilities and other private requests through our email.
Please do not report any bugs / feature requests here - they will be ignored and deleted.
[homarr-labs@proton.me](mailto:homarr-labs@proton.me)
## Discord
Use the [Homarr Discord server](https://discord.gg/aCsmEV5RgA) for community support, release discussion, and dashboard
showcases.
We have a Discord Server, where we announce new releases, discuss features and bugs and show off our dashboards.
There are over 4'000 members and we're happy to answer your questions or concerns.
You're also open to discuss new / upcoming features and any offtopic selfhosting content.
## Email
Use [homarr-labs@proton.me](mailto:homarr-labs@proton.me) for security reports, sponsorships, and other private matters.
Bug reports and feature requests belong on GitHub.
[_![](https://discordapp.com/api/guilds/972958686051962910/widget.png?style=banner2)_](https://discord.gg/aCsmEV5RgA)
+41 -14
View File
@@ -5,24 +5,51 @@ tags:
- Translations
---
# Translations
We want to make Homarr accessible for everyone.
But to do that, we need your help.
We are running a community program that enables everyone to translate Homarr into their language – no programming required.
We are using [Crowdin](https://crowdin.com/) to manage our translations.
You can find the translations [here](https://translate.homarr.dev/).
Homarr translations are managed on [Crowdin](https://translate.homarr.dev/). Select a language, translate strings in
context, and submit them for proofreading. Approved translations ship with a later Homarr release.
## How to translate
## Translate from Homarr
### Using crowdin's online platform
Select **Crowdin (live translations)** as the application language, connect a Crowdin account, and choose the target
language. Select text in Homarr or use Crowdin's in-context panel to edit a string.
Go to the [Crowdin translations](https://translate.homarr.dev/) and select a language you want to translate.
You can then enter the translation mode and start translating.
After you've submitted your translations, a Proofreader will review your changes.
After a translation has been approved, it will be merged to the language development branch.
We merge this one periodically to the development branch.
After the next update of Homarr, all changes from the development branch will be merged, including the changes in the translations.
Live translation submits changes to Crowdin; it does not update the installed Homarr image. Crowdin's preview option can
show pending translations before the next Homarr release.
### Using live translate
You can also help translate homarr directly from the app!
For this you can simply change your language to "Crowdin (live translations)"
![](./img/crowdin-language-select.png)
Then you'll be prompted to connect to your Crowdin account and choose the language in which you want to transalte.
You can then start translating directly from the app by selecting some text on the app.
If some text cannot be selected to be edited with the live translation tool, you can still translate it inside of the In-Context popup at the Home tab, it will be red if it's not translated yet.
![](./img/crowdin-live-translate.png)
:::caution
Although you are translating directly from the app, your translations will not be directly be available inside of your language, they will be in the next update of Homarr, because we have to push the new translations to a new docker image.
If you really want to use them you can enable the "preview transaltions" option in the live translate settings, but be aware that this is not recommended for performance reasons.
:::
:::tip
Do you want to become a proofreader? [Let us know on our Discord](/docs/community/get-in-touch#discord)! We will add you to the proofreader team. You can then review the translations and approve them. **Note:** You must speak the language fluently, have it as your native language or lots of experience with it.
:::
## Guidelines
- Preserve the meaning and context of the English source.
- Keep terminology consistent with existing translations.
- Do not add punctuation, formatting, or explanatory text that is absent from the source.
- Improve an existing translation only when the replacement is more accurate.
Contact the [Discord community](/docs/community/get-in-touch#discord) to help proofread a language.
1. Translate with context: Some translations can't be translated 1:1 - they need context. Ensure that your translations make sense in the context.
2. Keep things simple and original - no additional characters are needed.
3. Don't translate the same thing twice - if you see a translation that is already translated, don't translate it again unless you have a better translation.
4. Keep your translations as close to the original as possible.
@@ -6,76 +6,393 @@ tags:
- Configuration
---
On first launch, onboarding creates the administrator, connects optional services, and builds the first board. A
compatible SQLite backup can also be restored from the welcome screen.
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";
If onboarding returns after a restart, check that `/appdata` is writable and persisted. See the
[Docker installation guide](/docs/getting-started/installation/docker).
:::tip
## Core concepts
Read our [glossary](/docs/getting-started/glossary) if you are new to Homarr.
### Boards
:::
A **board** is a dashboard page containing apps, widgets, and Containers. Boards can be public or restricted to users
and groups. Each user can choose a home board.
Congratulations on installing Homarr. Now it's time to get yourself familiar with the concepts of Homarr and how to use it.
![Board management with board previews](./img/after-installation-boards.webp)
## Onboarding
See [Boards](/docs/management/boards) for responsive layouts, access control, appearance, and home-board settings.
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.
### Apps
:::note
An **app** is a saved shortcut: a name, URL, icon, and optional status check. Add an App tile or a Bookmarks widget to
show apps on a board.
If you see this screen repeatitly after you restarted the Homarr container, your Docker mounting paths may be missing,
incorrect or read-only. Read [our installation instructions](/docs/getting-started/installation/docker) again to fix this issue.
![App management with saved shortcuts](./img/after-installation-apps.webp)
:::
See [Apps](/docs/management/apps) or [Icons](/docs/advanced/icons).
You can either import a previous dashboard using the ZIP archive or start from scratch:
### Integrations
![Screenshot of Step 1 onboarding](./img/onboarding-step-1.png)
An **integration** is a server-side connection from Homarr to a supported service such as Sonarr, Home Assistant, or
Docker. Integrations provide data and actions to widgets. They can also be linked to apps.
You may refresh anytime and continue from a different device - the onboarding is saved immediately on the server for your convenience.
![Integration catalog with compatible widgets](./img/after-installation-integrations.webp)
### Starting from scratch
See [Managing integrations](/docs/management/integrations) and the
[integration catalog](/docs/category/integrations).
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.
### Widgets
<img src={imageStartFromScratchStepAdminUser} alt={"Screenshot form to create admin user"} width={300} />
A **widget** is a board tile. Some widgets work on their own; others require one or more integrations. Configuration,
supported integrations, and non-obvious limitations are documented on each widget page.
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.
![Board in edit mode with the add-content menu](./img/after-installation-widgets.webp)
<img
src={imageStartFromScratchStepExternalAdminGroup}
alt={"Screenshot form to create external admin group"}
width={300}
/>
See the [widget catalog](/docs/category/widgets).
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.
### Containers and rails
<img
src={imageStartFromScratchStepServerSettings}
alt={"Screenshot of analytics and indexing settings in the onboarding"}
width={300}
/>
A **Container** groups board items and can be nested. A **rail** reserves a fixed area at the left or right of a
responsive layout. Both are configured in board edit mode.
### Setting up integrations
![Board layout settings with fixed sidebars](./img/after-installation-containers-rails.webp)
After configuring server settings, the onboarding guides you through setting up your integrations.
See [Boards](/docs/management/boards#containers-and-rails).
#### Docker service discovery
### Users, groups, and permissions
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.
Users inherit permissions and default boards from their groups. Resource-specific access can further restrict boards
and integrations.
#### URL templating
![Group permission settings](./img/after-installation-users-permissions.webp)
You can define a base URL template to auto-fill integration URLs in bulk.
Two modes are supported:
See [Users and groups](/docs/management/users) and [single sign-on](/docs/advanced/single-sign-on).
- **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
## Explore further
#### API key helper links
- [Server settings](/docs/management/settings)
- [Assistant](/docs/management/assistant)
- [Custom Widgets](/docs/management/custom-widgets)
- [Community Workshop](/docs/workshop)
- [API](/docs/management/api) and [MCP](/docs/management/mcp)
- [Keyboard shortcuts](/docs/advanced/keyboard-shortcuts)
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.
See the [glossary](/docs/getting-started/glossary) for a compact list of Homarr terms.
#### 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
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`:
![](./img/manage-create-board-button.png)
A board must always have a name and can either be public or private.
Public boards are also accessible for users, that are not logged in ([see access control for boards](/docs/management/boards/#access-control)).
![](./img/manage-create-board-modal.png)
Congratulations - you've just created your first board. You can see boards, that you have access to, on this page:
![](./img/manage-view-board.png)
## Add your first app to Homarr
To add links to your dashboard, you must first add them as an "app".
Navigate to this page to add an app:
![](./img/manage-navigate-apps.png)
Next, click on the `New app` button. You'll need to provide a name and an icon to create an app.
The name must be at least one character long but you can provide any image URL for the icon.
Homarr ships with over 11K icons - so you can just search by entering a search text instead.
![](./img/manage-add-app-form.png)
After you created the app, you'll see in on the apps page:
![](./img/manage-created-app.png)
## Arrange and organize your board
Let's add your freshly created app, from the previous step, to the board.
To do this, edit your previously created board by clicking on the big `Open board` button:
![](./img/manage-view-board.png)
Here you have the following options in the header:
![](./img/manage-board-header.png)
- **Search**: Here you can search for data. You can search for integrations, boards, actions, sites and much more. Some actions may be context dependent (eg. will only show when certain widgets are on your page)
- **Edit mode**: Clicking this button will toggle the edit mode. In this mode you can edit the contents of your board.
- **Settings**: Clicking this button will redirect you to a settings page where you can adjust the board settings (page title, colors, ...).
- **Profile**: This avatar will be visible on every page. Clicking it will show a menu with actions.
When entering the edit mode, you'll get some additional buttons to work with:
<img src={manageBoardHeaderEditMode} alt={"Screenshot of buttons in the edit mode"} width={200} />
- **Add item**: Click this button to add new elements to your page.
- **Exit edit mode**: Click this button to save your changes and exit edit mode.
Outside edit mode, right-click a widget for its live status, refresh, settings, quick options, and supported actions.
Hold `Shift` while hovering a widget that supports advanced view to expand it in place while the rest of the board
stays visible but dimmed. Release `Shift` to close it. You can also focus that widget and press `Shift` + `Enter` to
keep its advanced view open until you close it. Widgets without a distinct advanced experience adapt in place.
To continue, click the option "New Item":
<img src={manageBoardHeaderAddItem} alt={"Screenshot of the add item menu"} width={200} />
Next, you can choose what item you'd like to add.
We will discuss the other options in the following sections - for now choose "App" to continue.
<img src={manageBoardHeaderChooseItem} alt={"Screenshot of choosing the app item in the modal"} width={300} />
As soon as you chose the app, the modal will close and you'll get the following item on your board:
<img src={manageBoardNoApp} alt="Screenshot of the No app element on the board" />
This is expected, because you haven't told Homarr what app it should show in this item.
To configure this, click on the small 3 dots at the top of the item in the edit mode and choose "Edit item" in the menu.
![](./img/manage-board-edit-app.gif)
In the modal you can edit the app that you want to display:
![](./img/manage-board-choose-app.png)
As soon as you save your changes, the tile on the board will refresh and display your app.
Use the resize handle at the bottom right to resize the tile.
Press and hold the handle, move the pointer, and release it when the tile has the size you want.
![](./img/resize-item.gif)
To move a tile in edit mode, press and hold its content, move it to the desired position, and release it.
Widget content is disabled while editing so its controls do not intercept the drag; use the tile's settings button for edit actions.
The tile snaps to the grid. Equally-sized tiles swap when one is dropped into the other's slot, while other neighboring tiles move out of the way automatically.
![](./img/move-item.gif)
Every `1 × 1` tile has a logical content size of `200 × 200` pixels.
Larger tiles use multiples of that fixed unit, with a consistent gap between cells.
Homarr scales the complete board uniformly, like zooming a page.
The complete canvas always fits the available width without horizontal board scrolling.
Below 100%, Homarr preserves the widget's readable typography and control density instead of shrinking each widget independently.
It does not squeeze or reflow the content of individual widgets, so a widget keeps the same design and proportions across screen sizes.
:::note
Viewing a board initially uses the lightweight, non-interactive layout.
For users with edit access, Homarr prepares drag-and-drop and resize controls in the background after the board becomes interactive.
If you enter edit mode before that finishes, the existing board stays visible until the editor is ready.
:::
:::tip
We recommend you to experiment for a few minutes, before you start building your actual dashboard.
This helps you to explore the capabilities of Homarr before you build your board in a suboptimal way.
:::
Keyboard users can focus a tile, press Enter or Space, then use arrow keys to move and Shift plus arrow keys to resize.
Press Escape to stop.
For precise numeric controls or touch input, choose **Move / resize item** from the tile's edit menu.
The dialog can also move the tile to another section or fixed sidebar:
![](./img/manage-board-move-resize-item-menu.png)
Be sure to save your changes by exiting edit mode!
![](./img/exit-edit-mode.png)
## Adding your first integration
Now it's time to integrate some basic data into your board.
To do this, we first need to create an integration.
An integration is a connection between Homarr and a third party application - usually something that you run and deploy yourself.
Navigate to `Manage` > `Integrations` (for example via your user menu).
As an example, we are going to use the public demo instance of **Dash.** - a popular application to monitor CPU / hardware usage.
Choose the **Dash.** integration from the list of available integrations:
![](./img/choose-integration.gif)
Next, choose a name (or leave it at default), enter `https://dash.mauz.dev/` as the URL in the form and click on `Test connection and create`.
Pressing the save button will perform a test request to the integration, to confirm that the networking setup is working as expected.
Sometimes invalid configurations, firewalls and other mistakes can lead to connectivity problems - [follow this guide here](/docs/management/integrations/#troubleshooting-connection) if you experience issues.
If connection was successful, you will be redirect back to the list of integrations.
Now let's embed it to your board (see next section).
:::note
Some integrations may require additional configuration, such as a username, password or API tokens.
Depending on your networking setup and applications, the connection from Homarr might also get blocked by firewalls,
the target application or your router.
:::
## Embed integration data using widgets
Let's add your integration, that you have configured in the section above, as a widget to your board.
First, navigate back to your board that you want to add it to.
Next, enter edit mode again and add another item to your board:
![](./img/manage-board-header-add-item-menu.png)
Instead of choosing an app, let's choose `System Health Monitoring` this time.
Click on "Add to board":
![](./img/manage-board-header-choose-system-health.png)
Resize the item slightly and then edit it using the three dots menu at the top:
![](./img/edit-system-health-widget.gif)
Next, choose your newly created Dash. integration in the integrations dropdown:
![](./img/manage-board-edit-system-health.png)
:::note
Some widgets may support multiple integrations at the same time.
If they do not support multiple integrations, you will be unable to select more than one.
:::
Confirm your changes by pressing save.
Editing a widget will trigger a data update and data will soon appear:
![](./img/widget-system-health.png)
As with every change, don't forget to save your changes using the exit edit mode button:
![](./img/exit-edit-mode.png)
## Organizing items with Containers
Containers are resizable areas for apps, widgets, and nested Containers.
The same Container works on the main canvas, inside a fixed rail, or inside another Container.
Each Container can configure:
- Its label and whether that label is visible.
- Allow or disable collapsing.
- Show or hide **Open all in new tabs**.
- Change its border color and custom CSS classes.
Open the add-content menu and choose **New Container**.
In edit mode, move and resize it like any other item, then drag items into or out of it.
Nested content stays within the Container's bounds.
While resizing, the solid outline follows the pointer continuously and the dashed grid preview shows the size that will be saved on release.
### Collapsing and opening apps
Users can collapse an eligible Container like an accordion in view mode without changing its saved expanded size.
The full-width header becomes the compact expansion bar, so the half-row state has no empty pill-shaped spacer.
Expanding it restores its bounds and makes room for its contents.
If **Open all in new tabs** is enabled, the Container menu can open every app link it contains.
### Fixed rails
Open the board's **Layout** settings to enable the left or right rail for each responsive layout.
Choose one, two, or three columns with the slider and use the preview to check the remaining canvas width.
The rail reserves those columns from the dashboard, so Homarr moves canvas items into the next available positions.
Rail content stays visible while the page scrolls and remains editable.
Each responsive layout keeps its own rail widths, item positions, and item sizes.
## Guided tours
After completing the onboarding, Homarr provides interactive guided tours to help you discover key features.
### Management tour
When you first visit the management area (`/manage`), Homarr walks you through the main pages: boards, apps, integrations, and users (admin only).
Each page highlights the list view and the create action so you know where to find and add things.
### Board tour
When you first open a board, a tour highlights the board header elements: search, edit mode, settings, board switcher, and user menu.
### Tour controls
- Press **Enter** (or click **Next**) to advance through tour steps
- Tours auto-start on first visit and do not repeat once completed
- Tours are skipped on mobile screens (below `sm` breakpoint)
- You can **reset tours** from the user settings page under **Manage > Users > [Your user] > General**
## Next steps
You're done with the basic setup 🔥. Now it's time to organize and customize even more.
We highly recommend you to experiment with different options, styles and configurations.
See these curated pages for your next steps:
- Create new apps using the [apps management](/docs/management/apps/)
- Customize your board or add more boards using the [boards management](/docs/management/boards/)
- Integrate software that you use using the [integrations management](/docs/management/integrations/)
- Choose from our beautiful and interactive [widgets](/docs/category/widgets)
- See [all supported integrations](/docs/category/integrations)
- Integrate with Homarr with your own application using [the API](/docs/management/api/)
:::danger
The Homarr Team will never ask for your credentials. Do not send people your credentials, as they manage the access to your applications and can cause harm to your device if they are abused.
:::
Loaded 100 of 2106 files, more files were not shown because too many files have changed in this diff. Show more