Compare commits

..
Author SHA1 Message Date
Thomas Camlong 4ac3a9d97c feat: add assistant widget to demo dashboard
Include the Homarr Assistant widget in the demo board so visitors can
try the assistant directly from the mock/demo dashboard. The widget
renders in its enabled state because demo mode reports the assistant as
available.
2026-08-07 15:01:23 +02:00
Thomas b14756874e fix: handle missing message in assistant generation telemetry (#6528) 2026-08-07 14:59:59 +02:00
ajnart 6bbb786ede style: auto-fix lint and format 2026-08-07 12:54:24 +00:00
Thomas Camlong 31d9e8d582 fix: prevent infinite render loop in assistant telemetry
RequestTelemetry derived thread message metadata inside the useAuiState
selector with messages.map(), returning a new array on every render.
useSyncExternalStore treats the new reference as a changed snapshot,
causing an infinite re-render loop (React error #185) while an assistant
message streams. Select the stable messages reference and memoize the
metadata derivation instead.
2026-08-07 14:53:05 +02:00
Thomas bc12aba433 Merge branch 'dev' into feat/homarr-assistant 2026-08-06 21:53:57 +02:00
Thomas eca8b4286d feat: mock Homarr Assistant in demo mode and fix board page crash (#6524) 2026-08-06 21:33:03 +02:00
ajnart fce950d406 test: wait for Home Assistant API readiness 2026-08-04 18:31:23 +02:00
ajnart bcdd56f714 feat: polish assistant conversations 2026-08-04 18:22:18 +02:00
ajnart 8e557fb9fd feat: improve assistant interactions 2026-08-04 13:21:39 +02:00
ajnart 1642a862d9 stabilize hydrated login actions 2026-08-03 18:37:05 +02:00
ajnart 62f5c1cbc3 stabilize integration router tests 2026-08-03 18:14:25 +02:00
ajnart 5e857c0e0a fix Docker dependency fetch scripts 2026-08-03 17:55:03 +02:00
ajnart f66d982ff3 fix duplicate release success flag 2026-08-03 17:35:28 +02:00
ajnart cd16ab8e84 Merge remote-tracking branch 'origin/dev' into feat/homarr-assistant 2026-08-03 17:34:33 +02:00
ajnart eaa7fd8bc3 clarify future Homarr provider 2026-08-03 17:25:14 +02:00
ajnart 4b62cbfb4b fix integration permission checks 2026-08-03 17:25:14 +02:00
ajnart 5155d7b742 fix merged dependency lockfile 2026-08-03 17:14:29 +02:00
ajnart 56a6d4989a Merge remote-tracking branch 'origin/dev' into feat/homarr-assistant
# Conflicts:
#	pnpm-lock.yaml
2026-08-03 17:08:45 +02:00
ajnart c43ab2ee56 fix assistant provider icon contrast 2026-08-03 17:05:12 +02:00
ajnart f7b291b465 add OpenRouter web search option 2026-08-03 16:59:27 +02:00
ajnart 35673f113e fix assistant widget configuration 2026-08-03 16:43:08 +02:00
ajnart bd3e4c33f3 enhance assistant batch responses 2026-08-03 15:48:41 +02:00
ajnart 6a98036c50 show assistant icon search previews 2026-08-03 15:36:10 +02:00
ajnart 59bed6b995 fix assistant streaming scroll lock 2026-08-03 15:30:10 +02:00
ajnart 5a3022a073 integrate assistant custom widget workflow 2026-08-03 15:22:08 +02:00
ajnart abc17e7c2a fix assistant prompt submission 2026-08-03 14:12:13 +02:00
ajnart c81a3b9432 refresh views after assistant changes 2026-08-03 13:38:20 +02:00
ajnart 8dd1d70900 improve assistant question placement 2026-08-03 13:07:07 +02:00
ajnart 9e0a6a758f fix assistant automatic approvals 2026-08-03 12:34:54 +02:00
ajnart 8f81de775e chore: remove unused ai sdk dependency 2026-08-02 16:57:11 +02:00
ajnart 8a2375b3ef fix: address assistant review feedback 2026-08-02 16:45:14 +02:00
ajnart cf6aefa48a fix: isolate assistant conversation state 2026-08-02 15:57:05 +02:00
ajnart d7969e1ec1 fix: harden assistant interactive tools 2026-08-02 14:20:40 +02:00
ajnart eff3168f1c feat: improve assistant tool workflows 2026-08-02 11:24:11 +02:00
ajnart 81a2f18c7c fix: map legacy media server options 2026-08-02 10:20:40 +02:00
ajnart 9eaadd83fb Merge remote-tracking branch 'origin/dev' into feat/homarr-assistant 2026-08-02 10:18:52 +02:00
ajnart 8bb0ddef42 fix: secure assistant generation telemetry 2026-08-02 09:09:01 +02:00
ajnart 04520cf67c feat: enrich assistant request telemetry 2026-08-02 08:51:48 +02:00
ajnart d562706c8b fix: sync assistant runtime preferences 2026-08-02 08:27:31 +02:00
ajnart 3245356f0f fix: align assistant management layout 2026-08-02 08:23:58 +02:00
ajnart 0d23bafa3d fix: hide inactive assistant scroll control 2026-08-02 08:18:05 +02:00
ajnart b7034e59c1 fix: polish assistant widget interactions 2026-08-01 18:25:48 +02:00
ajnart d111f7e981 fix: correct assistant widget docs metadata 2026-08-01 15:00:03 +02:00
ajnart 28b6321b1c style: auto-fix lint and format 2026-08-01 12:52:44 +00:00
ajnart 0a8e6ed194 feat: add assistant conversation widget 2026-08-01 14:51:20 +02:00
ajnart 6e28712ea7 fix: simplify assistant shortcut 2026-08-01 11:09:10 +02:00
ajnart 5458e7d647 feat: add assistant board styling tools 2026-08-01 10:39:41 +02:00
ajnart a5fa6d3afe fix: restore assistant approval flows 2026-08-01 09:54:11 +02:00
Thomas d0e28a5fc9 Merge branch 'dev' into feat/homarr-assistant 2026-07-31 13:55:31 +02:00
Thomas 1b55514d98 Merge branch 'dev' into feat/homarr-assistant 2026-07-31 13:32:03 +02:00
ajnart 9a2d6f692e feat: expose assistant global shortcuts 2026-07-31 06:58:39 +02:00
ajnart bf53a2a262 fix: keep assistant menu selection visible 2026-07-31 06:32:57 +02:00
ajnart 1d15b614f3 refactor: group assistant runtime controls 2026-07-31 06:00:49 +02:00
ajnart e6981a7f44 fix: invoke mutations before approval 2026-07-30 23:31:24 +02:00
ajnart 48dbdcd835 feat: redesign assistant management 2026-07-30 22:59:27 +02:00
ajnart 5fdf408378 docs: explain assistant search focus 2026-07-30 22:04:45 +02:00
ajnart 38e7d2c15f style: auto-fix lint and format 2026-07-30 20:02:54 +00:00
ajnart 82b146bdf6 feat: improve assistant confirmations 2026-07-30 22:01:21 +02:00
ajnart 2aefa23c36 feat: improve assistant background controls 2026-07-30 18:33:54 +02:00
ajnart 19cc115730 refactor: simplify assistant message controls 2026-07-30 17:33:43 +02:00
ajnart 429b587529 fix: complete assistant markdown lockfile 2026-07-30 17:03:14 +02:00
ajnart 93059c1e48 feat: render assistant content 2026-07-30 16:56:43 +02:00
ajnart af75bcd741 fix: send provider model ids 2026-07-30 16:00:09 +02:00
ajnart 6bd331bc73 fix: preserve assistant trigger focus 2026-07-30 14:33:29 +02:00
ajnart ee08870925 fix: address assistant capability review 2026-07-30 14:22:01 +02:00
ajnart 33e23021ae feat: expand assistant interaction capabilities 2026-07-30 14:09:15 +02:00
ajnart 9e6a0c7748 refactor: remove assistant chat archiving 2026-07-30 12:08:21 +02:00
ajnart 212ce64c04 fix: polish assistant mobile layout 2026-07-30 11:47:30 +02:00
ajnart 828f2ebecd fix: harden assistant floating panel 2026-07-30 11:17:08 +02:00
ajnart 0434f6ac10 feat: streamline assistant interactions 2026-07-30 11:06:16 +02:00
ajnart 7f946e45f4 fix: surface assistant stream errors 2026-07-30 10:01:36 +02:00
ajnart c10395fe5d fix: keep assistant in user menu 2026-07-30 09:37:52 +02:00
ajnart f3bdac4d23 fix: clarify assistant credential removal 2026-07-30 09:18:34 +02:00
ajnart be0496af5a fix: invalidate persisted assistant model state 2026-07-30 08:59:01 +02:00
ajnart 5da5433b58 fix: invalidate assistant models on discovery changes 2026-07-30 08:49:51 +02:00
ajnart 703c27c639 fix: gate assistant models on saved connection 2026-07-30 08:42:41 +02:00
ajnart 676ba6c885 fix: clear assistant credentials on destination changes 2026-07-30 08:34:44 +02:00
ajnart aa83fe4986 feat: promote assistant configuration 2026-07-30 08:25:00 +02:00
ajnart 1c3aac5e82 feat: expand assistant providers and polish experience 2026-07-30 00:39:22 +02:00
ajnart f90fb4512b fix: derive assistant approval key at runtime 2026-07-28 23:47:07 +02:00
ajnart 97fc9dda4c ci: limit turbo concurrency 2026-07-28 23:29:45 +02:00
ajnart 5c60489503 fix: hide closed assistant history 2026-07-28 23:19:31 +02:00
Thomas cfad8dd642 Merge branch 'dev' into feat/homarr-assistant 2026-07-28 23:13:43 +02:00
ajnart 36ae9f93ee fix: harden assistant integration 2026-07-28 22:56:28 +02:00
ajnart 00e8790ec2 feat: add Homarr assistant 2026-07-28 21:33:17 +02:00
779 changed files with 39858 additions and 24134 deletions

No files matched your search

@@ -1,44 +0,0 @@
---
name: documentation-sync
description: Keep Homarr documentation in sync with code changes. Use when modifying code that affects user-facing behavior, or when adding/modifying widgets, integrations, cron jobs, API routes, env vars, permissions, auth providers, or UI in apps/docs. Covers the docs/ mapping table, integration doc pattern, and widget doc pattern.
---
# Documentation Sync
When modifying code that affects user-facing behavior, you MUST also update the documentation in `apps/docs/`.
## What triggers a doc update
| Code change | Doc location |
| ---------------------- | ----------------------------------------------------------- |
| New integration | `apps/docs/docs/integrations/<slug>/index.mdx` + `index.ts` |
| New widget | `apps/docs/docs/widgets/<slug>/index.mdx` + `index.ts` |
| Changed API surface | `apps/docs/docs/management/api.mdx` |
| New/changed env vars | `apps/docs/docs/advanced/` |
| New CLI commands | `apps/docs/docs/advanced/command-line/` |
| Changed auth providers | `apps/docs/docs/advanced/` SSO pages |
| UI/UX changes | Relevant getting-started or management pages |
| New cron job | `apps/docs/docs/management/tasks.mdx` |
| Changed permissions | `apps/docs/docs/management/users.mdx` |
## Integration doc pattern
Each integration has a folder under `apps/docs/docs/integrations/<slug>/`:
- `index.ts` — exports a typed `IntegrationDefinition` with name, description, iconUrl, path
- `index.mdx` — uses shared components: `IntegrationHeader`, `IntegrationCapabilites`, `IntegrationSecrets`
- Import metadata from `@homarr/definitions` where possible
## Widget doc pattern
Each widget has a folder under `apps/docs/docs/widgets/<slug>/`:
- `index.ts` — exports a typed `WidgetDefinition` with icon, name, description, path, configuration
- `index.mdx` — uses shared components: `WidgetHeader`, `WidgetConfig`, `WidgetAdding`
## Development
- Run docs locally: `pnpm dev:docs` from root
- Build docs: `pnpm turbo build --filter=@homarr/docs`
- The docs app can import from `@homarr/definitions` for type-safe integration/widget metadata
- Docusaurus strict mode throws on broken links — fix any broken references before committing
-1
View File
@@ -1 +0,0 @@
../.agents/skills
@@ -1,6 +1,6 @@
---
name: codebase-context
description: Comprehensive Homarr codebase architecture and conventions reference. Use when orienting in the monorepo, understanding package dependency layers, the three runtime services and ports, tRPC router structure, multi-database support, widget/integration/cron system architecture, routing, auth providers, env vars, or key patterns before writing code.
description: Comprehensive Homarr codebase architecture and conventions reference
alwaysApply: true
---
# Homarr Codebase Context
@@ -27,7 +27,7 @@ Homarr is an open-source, self-hosted dashboard for managing homelab services. I
## Repository Structure
```text
```
homarr/
├── apps/
│ ├── nextjs/ # Main Next.js application (port 3000)
@@ -66,6 +66,8 @@ homarr/
│ ├── analytics/ # Server-side analytics (Umami)
│ ├── server-settings/ # Server setting keys/types
│ ├── settings/ # User-facing settings UI context
│ ├── old-import/ # Legacy Homarr import
│ ├── old-schema/ # Legacy Homarr zod schemas
│ └── cli/ # Node CLI for ops (brocli)
├── tooling/
│ ├── typescript/ # Base tsconfig
@@ -77,11 +79,11 @@ homarr/
## 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 |
| 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.
@@ -110,7 +112,6 @@ In Docker, nginx listens on **7575** and proxies to all three. The tasks service
### 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`
@@ -120,7 +121,6 @@ Selected at runtime via `DB_DRIVER` env (`better-sqlite3` | `mysql2` | `node-pos
### 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`
@@ -133,7 +133,6 @@ Integrations are defined in `packages/definitions/src/integration.ts` (`integrat
### 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
@@ -161,7 +160,6 @@ Configured via `AUTH_PROVIDERS` env. Supports: `credentials` (local), `ldap`, `o
## Environment Variables
Key env vars (from `.env.example`):
- `DB_DRIVER`: `better-sqlite3` | `mysql2` | `node-postgres`
- `DB_URL`: Database connection string
- `AUTH_SECRET`: NextAuth secret
@@ -193,3 +191,16 @@ Key env vars (from `.env.example`):
- 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
@@ -1,8 +1,3 @@
---
name: datatable-migration
description: Playbook for migrating tables from mantine-react-table (MRT) to mantine-datatable. Use when converting any table component from mantine-react-table to mantine-datatable, working on DataTable column/sort/expansion/context-menu code, or implementing responsive column visibility, column persistence, or transparent tables in widgets.
---
# Migrating Tables from mantine-react-table to mantine-datatable
This document captures every pattern, fix, and decision from the downloads widget rework (PR #6430). Use it as a playbook when porting other tables.
@@ -12,12 +7,10 @@ This document captures every pattern, fix, and decision from the downloads widge
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`
@@ -26,13 +19,13 @@ Current `mantine-react-table` consumers that could be migrated:
- `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
@@ -74,7 +67,6 @@ const columns: DataTableColumn<MyRow>[] = [
```
Key differences:
- `accessorKey` → `accessor`
- `header` → `title`
- `size` → `width`
@@ -126,6 +118,8 @@ const sortedData = useMemo(() => {
<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.
@@ -150,21 +144,13 @@ Create a CSS file alongside the component:
/* 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;
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;
background-color: color-mix(in srgb, var(--mantine-color-body) 60%, transparent) !important;
backdrop-filter: blur(8px);
}
@@ -186,16 +172,14 @@ Create a CSS file alongside the component:
/* Row expansion area */
.my-table .mantine-datatable-row-expansion-cell {
background-color: color-mix(
in srgb,
var(--mantine-color-body) 40%,
transparent
) !important;
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.
@@ -211,38 +195,11 @@ interface SizeConfig {
}
const SIZE_BREAKPOINTS: { maxWidth: number; config: SizeConfig }[] = [
{
maxWidth: 300,
config: {
fontSize: "xs",
iconSize: 12,
cellPadding: 2,
showSpeedColumns: false,
showTimeColumn: false,
showStateColumn: false,
},
},
{
maxWidth: 500,
config: {
fontSize: "xs",
iconSize: 14,
cellPadding: 4,
showSpeedColumns: false,
showTimeColumn: true,
showStateColumn: false,
},
},
{ maxWidth: 300, config: { fontSize: "xs", iconSize: 12, cellPadding: 2, showSpeedColumns: false, showTimeColumn: false, showStateColumn: false } },
{ maxWidth: 500, config: { fontSize: "xs", iconSize: 14, cellPadding: 4, showSpeedColumns: false, showTimeColumn: true, showStateColumn: false } },
];
const DEFAULT_SIZE_CONFIG: SizeConfig = {
fontSize: "sm",
iconSize: 14,
cellPadding: 4,
showSpeedColumns: true,
showTimeColumn: true,
showStateColumn: true,
};
const DEFAULT_SIZE_CONFIG: SizeConfig = { fontSize: "sm", iconSize: 14, cellPadding: 4, showSpeedColumns: true, showTimeColumn: true, showStateColumn: true };
function getSizeConfig(width: number): SizeConfig {
for (const { maxWidth, config } of SIZE_BREAKPOINTS) {
@@ -258,9 +215,7 @@ const size = useMemo(() => getSizeConfig(width), [width]);
Then use a visibility check lookup table for columns:
```typescript
const columnVisibilityChecks: Partial<
Record<string, (ctx: ColumnContext) => boolean>
> = {
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,
@@ -275,6 +230,8 @@ function shouldIncludeColumn(accessor: string, ctx: ColumnContext): boolean {
}
```
---
## 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.
@@ -306,6 +263,15 @@ Even hidden options need translation keys or the translation test fails:
"widget.mywidget.option.columnWidths.label": "Column widths"
```
### Old-import mappings
Add to `packages/old-import/src/widgets/options.ts`:
```typescript
columnOrder: () => undefined,
columnWidths: () => undefined,
```
### Dynamic storeKey
Use a key that includes `itemId` and sorted column names. This prevents cross-widget localStorage bleeding:
@@ -319,13 +285,8 @@ const storeKey = `mytable-${itemId ?? "preview"}-${[...options.columns].toSorted
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 { effectiveColumns, columnsOrder, columnsWidth, setColumnsOrder, setMultipleColumnWidths } =
useDataTableColumns<MyRow>({ key: storeKey, columns });
const lastStoreKey = useRef(storeKey);
const hydrated = useRef(false);
@@ -339,22 +300,11 @@ useEffect(() => {
if (savedOrder.length > 0) setColumnsOrder(savedOrder);
if (Object.keys(savedWidths).length > 0) {
setMultipleColumnWidths(
Object.entries(savedWidths).map(([accessor, w]) => ({
accessor,
width: w,
})),
Object.entries(savedWidths).map(([accessor, w]) => ({ accessor, width: w })),
);
}
requestAnimationFrame(() => {
hydrated.current = true;
});
}, [
storeKey,
savedOrder,
savedWidths,
setColumnsOrder,
setMultipleColumnWidths,
]);
requestAnimationFrame(() => { hydrated.current = true; });
}, [storeKey, savedOrder, savedWidths, setColumnsOrder, setMultipleColumnWidths]);
```
### Persist localStorage → server
@@ -367,26 +317,22 @@ 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);
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 (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 v = entry[key as keyof typeof entry];
if (typeof v === "number") widthMap[key] = v;
else if (typeof v === "string" && v.endsWith("px"))
widthMap[key] = parseInt(v, 10);
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) });
}
@@ -395,6 +341,8 @@ useEffect(() => {
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:
@@ -426,41 +374,15 @@ const handleContextMenu = useCallback(
The menu component uses `Portal` for correct z-index:
```tsx
function RowContextMenu({
state,
onClose,
}: {
state: ContextMenuState;
onClose: () => void;
}) {
function RowContextMenu({ state, onClose }: { state: ContextMenuState; onClose: () => void }) {
return (
<Portal>
<Menu
opened
onClose={onClose}
closeOnItemClick={false}
position="right-start"
offset={0}
>
<Menu opened onClose={onClose} closeOnItemClick={false} position="right-start" offset={0}>
<Menu.Target>
<Box
style={{
position: "fixed",
left: state.x,
top: state.y,
width: 0,
height: 0,
}}
/>
<Box style={{ position: "fixed", left: state.x, top: state.y, width: 0, height: 0 }} />
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
onClick={() => {
/* action */ onClose();
}}
>
Action
</Menu.Item>
<Menu.Item onClick={() => { /* action */ onClose(); }}>Action</Menu.Item>
</Menu.Dropdown>
</Menu>
</Portal>
@@ -470,6 +392,8 @@ function RowContextMenu({
**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
@@ -489,6 +413,8 @@ function RowContextMenu({
**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
@@ -549,11 +475,8 @@ 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;
}
try { return JSON.parse(value) as T; }
catch { return fallback; }
}
```
@@ -571,19 +494,20 @@ If the widget has configurable default sort, sync it:
```typescript
useEffect(() => {
setSortStatus({
columnAccessor: options.defaultSort,
direction: defaultSortDirection,
});
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`
@@ -594,14 +518,12 @@ useEffect(() => {
```typescript
const mutationOptions = {
onSettled: () => void utils.myQuery.invalidate(),
onError: () =>
showErrorNotification({
title: t("errors.actionFailed"),
message: t("errors.actionFailedMessage"),
}),
onError: () => showErrorNotification({ title: t("errors.actionFailed"), message: t("errors.actionFailedMessage") }),
};
```
---
## 10. i18n
- No hardcoded strings. Every user-visible string goes through `useScopedI18n`
@@ -612,6 +534,8 @@ const mutationOptions = {
- 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:
@@ -626,6 +550,8 @@ if (isEditMode) rowContextMenuHandler = undefined;
<DataTable style={{ pointerEvents: tablePointerEvents }} />
```
---
## 12. DataTable Props Reference (Commonly Used)
```typescript
@@ -664,6 +590,8 @@ if (isEditMode) rowContextMenuHandler = undefined;
/>
```
---
## 13. Layout Stability
- Don't use `table-layout: fixed` — it fights mantine-datatable's resize logic
@@ -672,6 +600,8 @@ if (isEditMode) rowContextMenuHandler = undefined;
- 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`)
@@ -680,12 +610,13 @@ if (isEditMode) rowContextMenuHandler = undefined;
4. Add CSS file for transparent backgrounds (if widget context)
5. Add responsive column visibility (if widget context)
6. Add hidden options + hydration logic (if column persistence needed)
7. Add translation keys (including hidden option labels)
8. Add context menu if row actions exist
9. Add row expansion if detail view exists
10. Add error handling on all mutations
11. Add accessibility attributes
12. Test at multiple widget widths
13. Update docs in `apps/docs/docs/widgets/` if applicable
14. Run `pnpm turbo typecheck`
15. Run translation spec — catches missing keys
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
@@ -1,14 +1,13 @@
---
name: mcp-integration
description: Expose tRPC procedures as MCP tools for AI clients. Use when modifying procedures in packages/api/src/router, adding a new tRPC procedure or router, or deciding what to expose via Homarr's MCP interface. Covers .meta({ mcp }), router registration in packages/api/src/mcp.ts, input schema rules, and what to expose vs skip.
description: How to expose tRPC procedures as MCP tools for AI clients
globs: packages/api/src/router/**/*.ts
alwaysApply: false
---
# MCP Integration
# 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.
Exposing a procedure makes it callable by any AI client with an API key. Before adding `.meta({ mcp })`, review what the tool exposes: authorization requirements, sensitive data, tenant isolation, auditability, and whether the action is destructive. Prefer exposing read-only queries over mutations unless the mutation is safe and permission-scoped. When in doubt, ask for a security review before shipping a new MCP tool.
## Adding MCP to a procedure
Add `.meta({ mcp: { enabled: true, description: "..." } })` to the procedure chain:
@@ -17,7 +16,7 @@ Add `.meta({ mcp: { enabled: true, description: "..." } })` to the procedure cha
// 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(10) }))
.input(z.object({ limit: z.number().default(50) }))
.query(async ({ ctx, input }) => { ... })
// Write tool (mutation)
@@ -39,27 +38,23 @@ The `.meta()` call must come **before** `.input()` and `.query()`/`.mutation()`
The description is the **only thing** an AI sees to decide when and how to use the tool. Write it like you're explaining to a colleague who has never used Homarr.
**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:**
```text
```
"Get calendar events for upcoming and recent media releases. Fetches from all connected Sonarr (TV), Radarr (movies), Lidarr (music), and Readarr (books) integrations. Requires integrationIds from integration_all"
```
**Bad:**
```text
```
"Get calendar events"
```
**For tools that return permission fields**, explain what they mean:
```text
```
"List all integrations. Returns permissions.hasUseAccess (read) and permissions.hasInteractAccess (actions) — false means the API key lacks that permission, not an error"
```
@@ -82,13 +77,11 @@ export const mcpRouter = createTRPCRouter({
## 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
+1 -2
View File
@@ -17,8 +17,7 @@ coverage
dev
.build
e2e
apps/docs/*
!apps/docs/package.json
apps/docs
apps/nextjs/.next
.env
.env.*
-2
View File
@@ -33,8 +33,6 @@ body:
options:
# The below comment is used to insert a new version with on-release.yml
#NEXT_VERSION#
- 1.75.0
- 1.74.0
- 1.73.0
- 1.72.0
- 1.71.0
+35
View File
@@ -0,0 +1,35 @@
name: "[Dependency Updates] Auto Approve"
on:
pull_request:
types: [opened, synchronize]
permissions: {}
jobs:
approve-automatic-prs:
runs-on: ubuntu-latest
timeout-minutes: 2
# 158783068: homarr-renovate[bot]
# 190541745: homarr-crowdin[bot]
# 210161987: homarr-update-contributors[bot]
# 175486441: homarr-releases[bot]
# Extracted from https://api.github.com/users/<username>
if: github.actor_id == 158783068 || github.actor_id == 190541745 || github.actor_id == 210161987 || github.actor_id == 175486441
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Obtain token
id: obtainToken
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
client-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
permission-pull-requests: write # required to approve pull request
- name: Install GitHub CLI
run: sudo apt-get install -y gh
- name: Approve automatic PRs
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
run: |
gh pr review ${{github.event.pull_request.number}} --approve --body "Automatically approved by GitHub Action"
@@ -1,43 +0,0 @@
name: "[Bots] Reconcile Auto-merge"
on:
schedule:
- cron: "*/30 * * * *"
workflow_dispatch:
permissions: {}
jobs:
reconcile:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Obtain token
id: obtainToken
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
client-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
permission-pull-requests: write # required to enable auto-merge
- name: Install GitHub CLI
run: sudo apt-get install -y gh
- name: Re-enable auto-merge for prepared bot PRs
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
run: |
# 158783068: homarr-renovate[bot]
# 190541745: homarr-crowdin[bot]
# 210161987: homarr-update-contributors[bot]
# 175441: homarr-releases[bot]
gh pr list --repo "${{ github.repository }}" --base dev --state open --limit 100 \
--json number,author,reviewDecision,mergeStateStatus,autoMergeRequest \
--jq '.[] | select(.author.login as $login | ["app/homarr-renovate", "app/homarr-crowdin", "app/homarr-update-contributors", "app/homarr-releases"] | index($login)) | select(.reviewDecision == "APPROVED") | select(.mergeStateStatus == "CLEAN") | select(.autoMergeRequest == null) | .number' \
| while read -r pr; do
echo "Re-enabling auto-merge on PR #$pr"
gh pr merge "$pr" --auto --squash
done
-47
View File
@@ -1,47 +0,0 @@
name: "[Bots] Approve and Auto-merge"
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
permissions: {}
jobs:
approve-and-automerge:
runs-on: ubuntu-latest
timeout-minutes: 5
# 158783068: homarr-renovate[bot]
# 190541745: homarr-crowdin[bot]
# 210161987: homarr-update-contributors[bot]
# 175441: homarr-releases[bot]
# Extracted from https://api.github.com/users/<username>
if: |
(github.actor_id == 158783068 || github.actor_id == 190541745 || github.actor_id == 210161987 || github.actor_id == 175441) &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.pull_request.base.ref == 'dev'
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Obtain token
id: obtainToken
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
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
run: sudo apt-get install -y gh
- name: Approve and enable auto-merge
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
run: |
PR_NUMBER="${{ github.event.pull_request.number }}"
if [ "$(gh pr view "$PR_NUMBER" --json reviewDecision --jq .reviewDecision)" != "APPROVED" ]; then
gh pr review "$PR_NUMBER" --approve --body "Automatically approved by Homarr bot policy"
fi
if [ "$(gh pr view "$PR_NUMBER" --json autoMergeRequest --jq '.autoMergeRequest != null')" != "true" ]; then
gh pr merge "$PR_NUMBER" --auto --squash
fi
+1 -1
View File
@@ -82,7 +82,7 @@ jobs:
run: cp .env.example .env
- name: Lint, typecheck, and build affected workspaces
run: pnpm turbo run lint typecheck build --affected --continue=dependencies-successful --concurrency=2
run: pnpm turbo run lint typecheck build --affected --continue=dependencies-successful --concurrency=4
- name: Validate workspace configuration
run: pnpm lint:ws
+1 -3
View File
@@ -71,12 +71,10 @@ apps/nextjs/public/images/background.png
# personal agent config
.opencode
.agents/*
!.agents/skills/
.agents/
skills-lock.json
# docusaurus
apps/docs/.docusaurus
apps/docs/build
.pnpm-store
benchmark-results
-8
View File
@@ -1,8 +0,0 @@
{
"mcpServers": {
"mantine": {
"command": "npx",
"args": ["-y", "@mantine/mcp-server"]
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Loaded 100 of 779 files, more files were not shown because too many files have changed in this diff. Show more