Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
465c813b5f | ||
|
|
73a742154e | ||
|
|
993a267e0c | ||
|
|
f1c49d1fbf | ||
|
|
95c66c61eb | ||
|
|
ec2f8c6c2c | ||
|
|
05ba57c2fa | ||
|
|
ddbf05524b | ||
|
|
df438b4cd2 | ||
|
|
a1f91eb385 | ||
|
|
8d5ffb2dd9 | ||
|
|
56e6b23af6 | ||
|
|
5488bf4d18 | ||
|
|
621ea07549 | ||
|
|
f64c01456b | ||
|
|
56198c3f02 | ||
|
|
ca1bb6f341 | ||
|
|
bf6b6663cd | ||
|
|
5c2d86767f | ||
|
|
7c75642d4d | ||
|
|
7652eb8c1c | ||
|
|
e761c10f32 | ||
|
|
2513e8454f | ||
|
|
f6137b2dcc | ||
|
|
425dd90871 | ||
|
|
527e911eef | ||
|
|
ff2cdecc86 | ||
|
|
742bde1d27 | ||
|
|
64febbce12 | ||
|
|
98bb1aecf8 | ||
|
|
06c489bd48 | ||
|
|
464d98e124 | ||
|
|
987c9ade18 | ||
|
|
6141d505e4 | ||
|
|
6178843198 | ||
|
|
a7006d746e | ||
|
|
06724cfeb2 | ||
|
|
131a5558b5 | ||
|
|
b32ad82850 | ||
|
|
622e1dcfa5 | ||
|
|
9674bde758 | ||
|
|
b479ada14f | ||
|
|
bff3383ac9 | ||
|
|
d865733838 | ||
|
|
85fcea3837 | ||
|
|
f76a0f240a | ||
|
|
637f03e107 | ||
|
|
55214ccbc5 | ||
|
|
778533a62e | ||
|
|
b4fba47a23 | ||
|
|
76ddfceb13 | ||
|
|
2f6b96e5ba | ||
|
|
f1b7a5146a | ||
|
|
05292ae1f5 | ||
|
|
48fc8db2b4 | ||
|
|
018f84f80d | ||
|
|
d58a47b776 | ||
|
|
797bdd001f | ||
|
|
49314533bf | ||
|
|
27ca058088 | ||
|
|
f4449f9581 | ||
|
|
b52bc03c8d | ||
|
|
edb56f3fcd | ||
|
|
fcc5130068 | ||
|
|
c7751e293d | ||
|
|
bd669a1a8f | ||
|
|
9b5b79525f | ||
|
|
426929556f | ||
|
|
ade640dcea | ||
|
|
61128ddcc8 | ||
|
|
6a3cee8107 | ||
|
|
06d9c07d22 | ||
|
|
49d0f1a141 | ||
|
|
ac73b9d891 | ||
|
|
489980814c | ||
|
|
14654acced | ||
|
|
2885f7a297 | ||
|
|
906c9f11fa | ||
|
|
3419805a49 | ||
|
|
51a6cbe6d4 | ||
|
|
d08eb2afa6 | ||
|
|
91a6b19ba1 | ||
|
|
e66979bab6 | ||
|
|
4ef7641cb2 | ||
|
|
f33fc838cf | ||
|
|
f0019fa213 | ||
|
|
ba8536b36d | ||
|
|
965aec5155 | ||
|
|
387db030cd | ||
|
|
8e557c9924 | ||
|
|
f26e60edf0 | ||
|
|
7bbfd74454 | ||
|
|
018b1ea4da | ||
|
|
14c3fa2e76 | ||
|
|
9168f8114b | ||
|
|
9cabb42edb | ||
|
|
bfba3c9215 | ||
|
|
7184bc09c1 | ||
|
|
9bb0c0f811 | ||
|
|
cf0e469465 | ||
|
|
8e7fa782b3 | ||
|
|
97652802f0 | ||
|
|
1706289124 | ||
|
|
dd23de7aaa | ||
|
|
f1602f67d4 | ||
|
|
c03cd97d3a | ||
|
|
a8e9a074d3 | ||
|
|
1e9d3e0656 | ||
|
|
01e0f3612d | ||
|
|
0015a6d88d | ||
|
|
77dc317da2 | ||
|
|
6aeda3faaf | ||
|
|
ac6a0c9254 | ||
|
|
ca358d9f8b | ||
|
|
03e0683892 | ||
|
|
6ac4c6a7b9 | ||
|
|
a0e0bf65ff | ||
|
|
bc018e1fe0 | ||
|
|
5e39c8813a | ||
|
|
06719cdef0 | ||
|
|
5144e14da4 | ||
|
|
8528d5068b | ||
|
|
c6c4428f7d | ||
|
|
7e4cb0201b | ||
|
|
7887ce12c0 | ||
|
|
2d450239ac | ||
|
|
bf4ccd8cb3 |
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
description: Comprehensive Homarr codebase architecture and conventions reference
|
name: codebase-context
|
||||||
alwaysApply: true
|
description: Comprehensive Homarr codebase architecture and conventions reference. Use when orienting in the monorepo, understanding package dependency layers, the three runtime services and ports, tRPC router structure, multi-database support, widget/integration/cron system architecture, routing, auth providers, env vars, or key patterns before writing code.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Homarr Codebase Context
|
# Homarr Codebase Context
|
||||||
@@ -27,7 +27,7 @@ Homarr is an open-source, self-hosted dashboard for managing homelab services. I
|
|||||||
|
|
||||||
## Repository Structure
|
## Repository Structure
|
||||||
|
|
||||||
```
|
```text
|
||||||
homarr/
|
homarr/
|
||||||
├── apps/
|
├── apps/
|
||||||
│ ├── nextjs/ # Main Next.js application (port 3000)
|
│ ├── nextjs/ # Main Next.js application (port 3000)
|
||||||
@@ -66,8 +66,6 @@ homarr/
|
|||||||
│ ├── analytics/ # Server-side analytics (Umami)
|
│ ├── analytics/ # Server-side analytics (Umami)
|
||||||
│ ├── server-settings/ # Server setting keys/types
|
│ ├── server-settings/ # Server setting keys/types
|
||||||
│ ├── settings/ # User-facing settings UI context
|
│ ├── settings/ # User-facing settings UI context
|
||||||
│ ├── old-import/ # Legacy Homarr import
|
|
||||||
│ ├── old-schema/ # Legacy Homarr zod schemas
|
|
||||||
│ └── cli/ # Node CLI for ops (brocli)
|
│ └── cli/ # Node CLI for ops (brocli)
|
||||||
├── tooling/
|
├── tooling/
|
||||||
│ ├── typescript/ # Base tsconfig
|
│ ├── typescript/ # Base tsconfig
|
||||||
@@ -80,7 +78,7 @@ homarr/
|
|||||||
## Three Runtime Services
|
## Three Runtime Services
|
||||||
|
|
||||||
| Service | Port | Role |
|
| Service | Port | Role |
|
||||||
|---------|------|------|
|
| --------- | ---- | -------------------------------------------- |
|
||||||
| Next.js | 3000 | Main web app (SSR + API routes) |
|
| Next.js | 3000 | Main web app (SSR + API routes) |
|
||||||
| WebSocket | 3001 | tRPC subscriptions via `ws` |
|
| WebSocket | 3001 | tRPC subscriptions via `ws` |
|
||||||
| Tasks | 3002 | Cron job runner + management API via Fastify |
|
| Tasks | 3002 | Cron job runner + management API via Fastify |
|
||||||
@@ -112,6 +110,7 @@ In Docker, nginx listens on **7575** and proxies to all three. The tasks service
|
|||||||
### Multi-Database Support
|
### Multi-Database Support
|
||||||
|
|
||||||
Drizzle schemas exist in three parallel implementations:
|
Drizzle schemas exist in three parallel implementations:
|
||||||
|
|
||||||
- `packages/db/schema/sqlite.ts`
|
- `packages/db/schema/sqlite.ts`
|
||||||
- `packages/db/schema/mysql.ts`
|
- `packages/db/schema/mysql.ts`
|
||||||
- `packages/db/schema/postgresql.ts`
|
- `packages/db/schema/postgresql.ts`
|
||||||
@@ -121,6 +120,7 @@ Selected at runtime via `DB_DRIVER` env (`better-sqlite3` | `mysql2` | `node-pos
|
|||||||
### Widget System
|
### Widget System
|
||||||
|
|
||||||
Widgets are registered in `packages/widgets/src/index.tsx` as `widgetImports` (must satisfy `Record<WidgetKind, ...>`). Each widget:
|
Widgets are registered in `packages/widgets/src/index.tsx` as `widgetImports` (must satisfy `Record<WidgetKind, ...>`). Each widget:
|
||||||
|
|
||||||
- Has a `definition` created via `createWidgetDefinition(kind, { icon, createOptions, supportedIntegrations? })`
|
- Has a `definition` created via `createWidgetDefinition(kind, { icon, createOptions, supportedIntegrations? })`
|
||||||
- Uses `optionsBuilder.from(factory => ({ ... }))` for typed options
|
- Uses `optionsBuilder.from(factory => ({ ... }))` for typed options
|
||||||
- Loads dynamically via `next/dynamic`
|
- Loads dynamically via `next/dynamic`
|
||||||
@@ -133,6 +133,7 @@ Integrations are defined in `packages/definitions/src/integration.ts` (`integrat
|
|||||||
### Cron Job System
|
### Cron Job System
|
||||||
|
|
||||||
Jobs defined in `packages/cron-jobs/src/jobs/`. Each job:
|
Jobs defined in `packages/cron-jobs/src/jobs/`. Each job:
|
||||||
|
|
||||||
- Uses `createCronJob` with a cron expression
|
- Uses `createCronJob` with a cron expression
|
||||||
- Publishes results to Redis channels
|
- Publishes results to Redis channels
|
||||||
- Widget subscriptions consume those channels via tRPC subscriptions
|
- Widget subscriptions consume those channels via tRPC subscriptions
|
||||||
@@ -160,6 +161,7 @@ Configured via `AUTH_PROVIDERS` env. Supports: `credentials` (local), `ldap`, `o
|
|||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
Key env vars (from `.env.example`):
|
Key env vars (from `.env.example`):
|
||||||
|
|
||||||
- `DB_DRIVER`: `better-sqlite3` | `mysql2` | `node-postgres`
|
- `DB_DRIVER`: `better-sqlite3` | `mysql2` | `node-postgres`
|
||||||
- `DB_URL`: Database connection string
|
- `DB_URL`: Database connection string
|
||||||
- `AUTH_SECRET`: NextAuth secret
|
- `AUTH_SECRET`: NextAuth secret
|
||||||
@@ -191,16 +193,3 @@ Key env vars (from `.env.example`):
|
|||||||
- Drag-and-drop via `@dnd-kit/*`
|
- Drag-and-drop via `@dnd-kit/*`
|
||||||
- `typescript.ignoreBuildErrors: true` in next.config (types checked separately via `typecheck`)
|
- `typescript.ignoreBuildErrors: true` in next.config (types checked separately via `typecheck`)
|
||||||
- `serverExternalPackages`: `dockerode`, `isomorphic-dompurify`, `jsdom`
|
- `serverExternalPackages`: `dockerode`, `isomorphic-dompurify`, `jsdom`
|
||||||
|
|
||||||
## Documentation (`apps/docs`)
|
|
||||||
|
|
||||||
Docusaurus 3 site at `apps/docs/`, package name `@homarr/docs`.
|
|
||||||
|
|
||||||
- Run with `pnpm dev:docs` from root
|
|
||||||
- Build with `pnpm turbo build --filter=@homarr/docs`
|
|
||||||
- Uses Tailwind CSS 3 for custom pages (not Mantine)
|
|
||||||
- Imports `@homarr/definitions` for type-safe integration/widget metadata
|
|
||||||
- When adding or modifying widgets, integrations, cron jobs, API routes, env vars, or user-facing features, you MUST also update corresponding docs in `apps/docs/docs/`
|
|
||||||
- Integration docs: `apps/docs/docs/integrations/<slug>/index.mdx` + `index.ts`
|
|
||||||
- Widget docs: `apps/docs/docs/widgets/<slug>/index.mdx` + `index.ts`
|
|
||||||
- Strict link checking: broken links/anchors cause build failure
|
|
||||||
@@ -0,0 +1,691 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
|
|
||||||
|
## 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"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 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);
|
||||||
|
}
|
||||||
|
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 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
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
name: documentation-sync
|
||||||
|
description: Keep Homarr documentation in sync with code changes. Use when modifying code that affects user-facing behavior, or when adding/modifying widgets, integrations, cron jobs, API routes, env vars, permissions, auth providers, or UI in apps/docs. Covers the docs/ mapping table, integration doc pattern, and widget doc pattern.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Documentation Sync
|
||||||
|
|
||||||
|
When modifying code that affects user-facing behavior, you MUST also update the documentation in `apps/docs/`.
|
||||||
|
|
||||||
|
## What triggers a doc update
|
||||||
|
|
||||||
|
| Code change | Doc location |
|
||||||
|
| ---------------------- | ----------------------------------------------------------- |
|
||||||
|
| New integration | `apps/docs/docs/integrations/<slug>/index.mdx` + `index.ts` |
|
||||||
|
| New widget | `apps/docs/docs/widgets/<slug>/index.mdx` + `index.ts` |
|
||||||
|
| Changed API surface | `apps/docs/docs/management/api.mdx` |
|
||||||
|
| New/changed env vars | `apps/docs/docs/advanced/` |
|
||||||
|
| New CLI commands | `apps/docs/docs/advanced/command-line/` |
|
||||||
|
| Changed auth providers | `apps/docs/docs/advanced/` SSO pages |
|
||||||
|
| UI/UX changes | Relevant getting-started or management pages |
|
||||||
|
| New cron job | `apps/docs/docs/management/tasks.mdx` |
|
||||||
|
| Changed permissions | `apps/docs/docs/management/users.mdx` |
|
||||||
|
|
||||||
|
## Integration doc pattern
|
||||||
|
|
||||||
|
Each integration has a folder under `apps/docs/docs/integrations/<slug>/`:
|
||||||
|
|
||||||
|
- `index.ts` — exports a typed `IntegrationDefinition` with name, description, iconUrl, path
|
||||||
|
- `index.mdx` — uses shared components: `IntegrationHeader`, `IntegrationCapabilites`, `IntegrationSecrets`
|
||||||
|
- Import metadata from `@homarr/definitions` where possible
|
||||||
|
|
||||||
|
## Widget doc pattern
|
||||||
|
|
||||||
|
Each widget has a folder under `apps/docs/docs/widgets/<slug>/`:
|
||||||
|
|
||||||
|
- `index.ts` — exports a typed `WidgetDefinition` with icon, name, description, path, configuration
|
||||||
|
- `index.mdx` — uses shared components: `WidgetHeader`, `WidgetConfig`, `WidgetAdding`
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
- Run docs locally: `pnpm dev:docs` from root
|
||||||
|
- Build docs: `pnpm turbo build --filter=@homarr/docs`
|
||||||
|
- The docs app can import from `@homarr/definitions` for type-safe integration/widget metadata
|
||||||
|
- Docusaurus strict mode throws on broken links — fix any broken references before committing
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
---
|
---
|
||||||
description: How to expose tRPC procedures as MCP tools for AI clients
|
name: mcp-integration
|
||||||
globs: packages/api/src/router/**/*.ts
|
description: Expose tRPC procedures as MCP tools for AI clients. Use when modifying procedures in packages/api/src/router, adding a new tRPC procedure or router, or deciding what to expose via Homarr's MCP interface. Covers .meta({ mcp }), router registration in packages/api/src/mcp.ts, input schema rules, and what to expose vs skip.
|
||||||
alwaysApply: false
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# MCP Integration Guide
|
# MCP Integration
|
||||||
|
|
||||||
Every new tRPC procedure that provides useful functionality should be exposed as an MCP tool so AI assistants (Claude, Cursor, etc.) can use it. This is a first-class feature of Homarr.
|
Every new tRPC procedure that provides useful functionality should be exposed as an MCP tool so AI assistants (Claude, Cursor, etc.) can use it. This is a first-class feature of Homarr.
|
||||||
|
|
||||||
|
Exposing a procedure makes it callable by any AI client with an API key. Before adding `.meta({ mcp })`, review what the tool exposes: authorization requirements, sensitive data, tenant isolation, auditability, and whether the action is destructive. Prefer exposing read-only queries over mutations unless the mutation is safe and permission-scoped. When in doubt, ask for a security review before shipping a new MCP tool.
|
||||||
|
|
||||||
## Adding MCP to a procedure
|
## Adding MCP to a procedure
|
||||||
|
|
||||||
Add `.meta({ mcp: { enabled: true, description: "..." } })` to the procedure chain:
|
Add `.meta({ mcp: { enabled: true, description: "..." } })` to the procedure chain:
|
||||||
@@ -16,7 +17,7 @@ Add `.meta({ mcp: { enabled: true, description: "..." } })` to the procedure cha
|
|||||||
// Read-only tool (query)
|
// Read-only tool (query)
|
||||||
myProcedure: protectedProcedure
|
myProcedure: protectedProcedure
|
||||||
.meta({ mcp: { enabled: true, description: "Get all items with their status and metadata" } })
|
.meta({ mcp: { enabled: true, description: "Get all items with their status and metadata" } })
|
||||||
.input(z.object({ limit: z.number().default(50) }))
|
.input(z.object({ limit: z.number().default(10) }))
|
||||||
.query(async ({ ctx, input }) => { ... })
|
.query(async ({ ctx, input }) => { ... })
|
||||||
|
|
||||||
// Write tool (mutation)
|
// Write tool (mutation)
|
||||||
@@ -38,23 +39,27 @@ The `.meta()` call must come **before** `.input()` and `.query()`/`.mutation()`
|
|||||||
The description is the **only thing** an AI sees to decide when and how to use the tool. Write it like you're explaining to a colleague who has never used Homarr.
|
The description is the **only thing** an AI sees to decide when and how to use the tool. Write it like you're explaining to a colleague who has never used Homarr.
|
||||||
|
|
||||||
**Must include:**
|
**Must include:**
|
||||||
|
|
||||||
- What the tool returns or does
|
- What the tool returns or does
|
||||||
- Which integrations/services it works with (if applicable)
|
- Which integrations/services it works with (if applicable)
|
||||||
- How to get required IDs (e.g., "Use integration_all to get the integrationId")
|
- How to get required IDs (e.g., "Use integration_all to get the integrationId")
|
||||||
- Permission requirements (if non-obvious)
|
- Permission requirements (if non-obvious)
|
||||||
|
|
||||||
**Good:**
|
**Good:**
|
||||||
```
|
|
||||||
|
```text
|
||||||
"Get calendar events for upcoming and recent media releases. Fetches from all connected Sonarr (TV), Radarr (movies), Lidarr (music), and Readarr (books) integrations. Requires integrationIds from integration_all"
|
"Get calendar events for upcoming and recent media releases. Fetches from all connected Sonarr (TV), Radarr (movies), Lidarr (music), and Readarr (books) integrations. Requires integrationIds from integration_all"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Bad:**
|
**Bad:**
|
||||||
```
|
|
||||||
|
```text
|
||||||
"Get calendar events"
|
"Get calendar events"
|
||||||
```
|
```
|
||||||
|
|
||||||
**For tools that return permission fields**, explain what they mean:
|
**For tools that return permission fields**, explain what they mean:
|
||||||
```
|
|
||||||
|
```text
|
||||||
"List all integrations. Returns permissions.hasUseAccess (read) and permissions.hasInteractAccess (actions) — false means the API key lacks that permission, not an error"
|
"List all integrations. Returns permissions.hasUseAccess (read) and permissions.hasInteractAccess (actions) — false means the API key lacks that permission, not an error"
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -77,11 +82,13 @@ export const mcpRouter = createTRPCRouter({
|
|||||||
## What to expose vs. what to skip
|
## What to expose vs. what to skip
|
||||||
|
|
||||||
**Expose:**
|
**Expose:**
|
||||||
|
|
||||||
- Data queries (list, search, get by ID)
|
- Data queries (list, search, get by ID)
|
||||||
- Actions users would ask an AI to do (toggle, create, delete, approve)
|
- Actions users would ask an AI to do (toggle, create, delete, approve)
|
||||||
- Status checks (health, stats, summaries)
|
- Status checks (health, stats, summaries)
|
||||||
|
|
||||||
**Skip:**
|
**Skip:**
|
||||||
|
|
||||||
- Subscriptions (WebSocket-only, not supported by MCP)
|
- Subscriptions (WebSocket-only, not supported by MCP)
|
||||||
- Internal procedures (session management, onboarding steps)
|
- Internal procedures (session management, onboarding steps)
|
||||||
- File upload/download procedures
|
- File upload/download procedures
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../.agents/skills
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
---
|
|
||||||
description: Keep documentation in sync with code changes
|
|
||||||
globs:
|
|
||||||
- packages/definitions/src/**
|
|
||||||
- packages/widgets/src/**
|
|
||||||
- packages/integrations/src/**
|
|
||||||
- packages/api/src/**
|
|
||||||
- apps/nextjs/src/**
|
|
||||||
- apps/docs/**
|
|
||||||
---
|
|
||||||
|
|
||||||
# Documentation Sync Rule
|
|
||||||
|
|
||||||
When modifying code that affects user-facing behavior, you MUST also update the documentation in `apps/docs/`.
|
|
||||||
|
|
||||||
## What triggers a doc update
|
|
||||||
|
|
||||||
| Code change | Doc location |
|
|
||||||
|---|---|
|
|
||||||
| New integration | `apps/docs/docs/integrations/<slug>/index.mdx` + `index.ts` |
|
|
||||||
| New widget | `apps/docs/docs/widgets/<slug>/index.mdx` + `index.ts` |
|
|
||||||
| Changed API surface | `apps/docs/docs/management/api.mdx` |
|
|
||||||
| New/changed env vars | `apps/docs/docs/advanced/` |
|
|
||||||
| New CLI commands | `apps/docs/docs/advanced/command-line/` |
|
|
||||||
| Changed auth providers | `apps/docs/docs/advanced/` SSO pages |
|
|
||||||
| UI/UX changes | Relevant getting-started or management pages |
|
|
||||||
| New cron job | `apps/docs/docs/management/tasks.mdx` |
|
|
||||||
| Changed permissions | `apps/docs/docs/management/users.mdx` |
|
|
||||||
|
|
||||||
## Integration doc pattern
|
|
||||||
|
|
||||||
Each integration has a folder under `apps/docs/docs/integrations/<slug>/`:
|
|
||||||
|
|
||||||
- `index.ts` — exports a typed `IntegrationDefinition` with name, description, iconUrl, path
|
|
||||||
- `index.mdx` — uses shared components: `IntegrationHeader`, `IntegrationCapabilites`, `IntegrationSecrets`
|
|
||||||
- Import metadata from `@homarr/definitions` where possible
|
|
||||||
|
|
||||||
## Widget doc pattern
|
|
||||||
|
|
||||||
Each widget has a folder under `apps/docs/docs/widgets/<slug>/`:
|
|
||||||
|
|
||||||
- `index.ts` — exports a typed `WidgetDefinition` with icon, name, description, path, configuration
|
|
||||||
- `index.mdx` — uses shared components: `WidgetHeader`, `WidgetConfig`, `WidgetAdding`
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
- Run docs locally: `pnpm dev:docs` from root
|
|
||||||
- Build docs: `pnpm turbo build --filter=@homarr/docs`
|
|
||||||
- The docs app can import from `@homarr/definitions` for type-safe integration/widget metadata
|
|
||||||
- Docusaurus strict mode throws on broken links — fix any broken references before committing
|
|
||||||
@@ -17,7 +17,8 @@ coverage
|
|||||||
dev
|
dev
|
||||||
.build
|
.build
|
||||||
e2e
|
e2e
|
||||||
apps/docs
|
apps/docs/*
|
||||||
|
!apps/docs/package.json
|
||||||
apps/nextjs/.next
|
apps/nextjs/.next
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ body:
|
|||||||
options:
|
options:
|
||||||
# The below comment is used to insert a new version with on-release.yml
|
# The below comment is used to insert a new version with on-release.yml
|
||||||
#NEXT_VERSION#
|
#NEXT_VERSION#
|
||||||
|
- 1.75.0
|
||||||
|
- 1.74.0
|
||||||
|
- 1.73.0
|
||||||
|
- 1.72.0
|
||||||
|
- 1.71.0
|
||||||
- 1.70.0
|
- 1.70.0
|
||||||
- 1.69.2
|
- 1.69.2
|
||||||
- 1.69.0
|
- 1.69.0
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
$schema: "https://docs.renovatebot.com/renovate-schema.json",
|
$schema: "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
extends: ["config:recommended"],
|
extends: ["config:recommended", ":dependencyDashboardApproval"],
|
||||||
packageRules: [
|
packageRules: [
|
||||||
{
|
{
|
||||||
matchPackageNames: ["^@homarr/"],
|
matchPackageNames: ["^@homarr/"],
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
rangeStrategy: "bump",
|
rangeStrategy: "bump",
|
||||||
automerge: false,
|
automerge: false,
|
||||||
baseBranchPatterns: ["dev"],
|
baseBranchPatterns: ["dev"],
|
||||||
dependencyDashboard: false,
|
dependencyDashboard: true,
|
||||||
// prevent special characters in branch names (otherwise git errors occur on windows)
|
// prevent special characters in branch names (otherwise git errors occur on windows)
|
||||||
branchNameStrict: true,
|
branchNameStrict: true,
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
ref: ${{ github.head_ref }}
|
ref: ${{ github.head_ref }}
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup
|
- name: Setup
|
||||||
uses: ./tooling/github/setup
|
uses: ./tooling/github/setup
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
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"
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
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
|
||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
TURBO_PLATFORM: linux/amd64
|
TURBO_PLATFORM: linux/amd64
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
if: '!env.ACT'
|
if: "!env.ACT"
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -81,11 +81,8 @@ jobs:
|
|||||||
- name: Copy environment defaults
|
- name: Copy environment defaults
|
||||||
run: cp .env.example .env
|
run: cp .env.example .env
|
||||||
|
|
||||||
- name: Check formatting in affected workspaces
|
|
||||||
run: pnpm turbo run format --affected --continue -- --check
|
|
||||||
|
|
||||||
- name: Lint, typecheck, and build affected workspaces
|
- name: Lint, typecheck, and build affected workspaces
|
||||||
run: pnpm turbo run lint typecheck build --affected --continue=dependencies-successful
|
run: pnpm turbo run lint typecheck build --affected --continue=dependencies-successful --concurrency=2
|
||||||
|
|
||||||
- name: Validate workspace configuration
|
- name: Validate workspace configuration
|
||||||
run: pnpm lint:ws
|
run: pnpm lint:ws
|
||||||
@@ -93,12 +90,8 @@ jobs:
|
|||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
run: pnpm test
|
run: pnpm test
|
||||||
|
|
||||||
- name: Report coverage
|
|
||||||
if: always() && !env.ACT
|
|
||||||
uses: davelosert/vitest-coverage-report-action@v2
|
|
||||||
|
|
||||||
container:
|
container:
|
||||||
name: Container, E2E, and memory
|
name: Container and E2E
|
||||||
needs: fast-gate
|
needs: fast-gate
|
||||||
if: |
|
if: |
|
||||||
github.event_name == 'merge_group' ||
|
github.event_name == 'merge_group' ||
|
||||||
@@ -111,7 +104,7 @@ jobs:
|
|||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
if: '!env.ACT'
|
if: "!env.ACT"
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
|
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
|
||||||
@@ -120,11 +113,11 @@ jobs:
|
|||||||
uses: ./tooling/github/setup
|
uses: ./tooling/github/setup
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
if: '!env.ACT'
|
if: "!env.ACT"
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
- name: Build container once
|
- name: Build container once
|
||||||
if: '!env.ACT'
|
if: "!env.ACT"
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
@@ -165,7 +158,7 @@ jobs:
|
|||||||
--tag homarr-ci --tag homarr-e2e "${secret_args[@]}" .
|
--tag homarr-ci --tag homarr-e2e "${secret_args[@]}" .
|
||||||
|
|
||||||
- name: Cache Playwright browsers
|
- name: Cache Playwright browsers
|
||||||
if: '!env.ACT'
|
if: "!env.ACT"
|
||||||
id: playwright-cache
|
id: playwright-cache
|
||||||
uses: actions/cache@v6
|
uses: actions/cache@v6
|
||||||
with:
|
with:
|
||||||
@@ -191,82 +184,6 @@ jobs:
|
|||||||
path: e2e/test-results/
|
path: e2e/test-results/
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
- name: Run memory regression test
|
|
||||||
env:
|
|
||||||
CONTAINER_NAME: homarr-mem-${{ github.run_id }}-${{ github.run_attempt }}
|
|
||||||
SECRET_ENCRYPTION_KEY: 0000000000000000000000000000000000000000000000000000000000000000
|
|
||||||
run: |
|
|
||||||
docker run -d --name "$CONTAINER_NAME" -p 127.0.0.1::7575 \
|
|
||||||
-e NO_EXTERNAL_CONNECTION=true \
|
|
||||||
-e SECRET_ENCRYPTION_KEY="$SECRET_ENCRYPTION_KEY" \
|
|
||||||
homarr-ci
|
|
||||||
PORT=$(docker port "$CONTAINER_NAME" 7575/tcp | sed 's/.*://')
|
|
||||||
[[ "$PORT" =~ ^[0-9]+$ ]] || { echo "ERROR: invalid port mapping: '$PORT'"; exit 1; }
|
|
||||||
URL="http://127.0.0.1:${PORT}"
|
|
||||||
|
|
||||||
healthy=false
|
|
||||||
for i in $(seq 1 30); do
|
|
||||||
if curl -sf "$URL/api/health/ready" >/dev/null 2>&1; then
|
|
||||||
echo "Container healthy after $((i * 2))s"
|
|
||||||
healthy=true
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$healthy" != "true" ]; then
|
|
||||||
echo "ERROR: Container never became healthy"
|
|
||||||
docker logs "$CONTAINER_NAME" --tail 100
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
warmup_ok=0
|
|
||||||
for _ in $(seq 1 5); do
|
|
||||||
curl -sf "$URL/" >/dev/null 2>&1 && warmup_ok=$((warmup_ok + 1))
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
if [ "$warmup_ok" -eq 0 ]; then
|
|
||||||
echo "ERROR: All warmup requests failed"
|
|
||||||
docker logs "$CONTAINER_NAME" --tail 50
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME" | grep -qx true || {
|
|
||||||
echo "ERROR: Container died before memory measurement"
|
|
||||||
docker logs "$CONTAINER_NAME" --tail 100
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
RAW_MEMORY=$(docker stats --no-stream --format '{{.MemUsage}}' "$CONTAINER_NAME" | sed 's| /.*||')
|
|
||||||
MEMORY_MB=$(echo "$RAW_MEMORY" | awk '
|
|
||||||
/GiB/ { gsub(/GiB/, ""); print $1 * 1024; next }
|
|
||||||
/MiB/ { gsub(/MiB/, ""); print $1; next }
|
|
||||||
/KiB/ { gsub(/KiB/, ""); print $1 / 1024; next }
|
|
||||||
{ print $1 / 1048576 }
|
|
||||||
')
|
|
||||||
|
|
||||||
echo "Memory usage: ${MEMORY_MB} MB (${RAW_MEMORY})"
|
|
||||||
[[ "$MEMORY_MB" =~ ^[0-9]+(\.[0-9]+)?$ ]] || {
|
|
||||||
echo "ERROR: non-numeric memory value: '$MEMORY_MB' (raw: '$RAW_MEMORY')"
|
|
||||||
docker logs "$CONTAINER_NAME" --tail 100
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
if awk "BEGIN { exit !($MEMORY_MB < 50) }"; then
|
|
||||||
echo "ERROR: Memory suspiciously low (${MEMORY_MB} MB) — container may have crashed"
|
|
||||||
docker logs "$CONTAINER_NAME" --tail 100
|
|
||||||
exit 1
|
|
||||||
elif awk "BEGIN { exit !($MEMORY_MB > 700) }"; then
|
|
||||||
echo "ERROR: Memory usage ${MEMORY_MB} MB exceeds 700 MB"
|
|
||||||
docker logs "$CONTAINER_NAME" --tail 100
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Clean up memory test container
|
|
||||||
if: always()
|
|
||||||
env:
|
|
||||||
CONTAINER_NAME: homarr-mem-${{ github.run_id }}-${{ github.run_attempt }}
|
|
||||||
run: docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
- name: Determine preview tag
|
- name: Determine preview tag
|
||||||
id: preview
|
id: preview
|
||||||
if: |
|
if: |
|
||||||
@@ -316,7 +233,7 @@ jobs:
|
|||||||
run: test -n "$PR_NUMBER" || { echo "pr_number is required for cleanup"; exit 1; }
|
run: test -n "$PR_NUMBER" || { echo "pr_number is required for cleanup"; exit 1; }
|
||||||
|
|
||||||
- name: Delete GHCR package version
|
- name: Delete GHCR package version
|
||||||
if: '!env.ACT'
|
if: "!env.ACT"
|
||||||
uses: actions/github-script@v8
|
uses: actions/github-script@v8
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
|
|||||||
@@ -71,10 +71,12 @@ apps/nextjs/public/images/background.png
|
|||||||
|
|
||||||
# personal agent config
|
# personal agent config
|
||||||
.opencode
|
.opencode
|
||||||
.agents/
|
.agents/*
|
||||||
|
!.agents/skills/
|
||||||
skills-lock.json
|
skills-lock.json
|
||||||
|
|
||||||
# docusaurus
|
# docusaurus
|
||||||
apps/docs/.docusaurus
|
apps/docs/.docusaurus
|
||||||
apps/docs/build
|
apps/docs/build
|
||||||
.pnpm-store
|
.pnpm-store
|
||||||
|
benchmark-results
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"mantine": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@mantine/mcp-server"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 40 KiB |