Compare commits

..
Author SHA1 Message Date
Thomas Camlong e5b8c18cd2 chore: edit renovate base branch 2026-06-12 14:03:07 +02:00
Thomas Camlong ab46d822cc fix: copy pnpm-workspace.yaml in prebuilt-debian Dockerfile
pnpm v11 reads allowBuilds from pnpm-workspace.yaml, not package.json.
Without it, better-sqlite3 build scripts are rejected.
2026-06-11 23:48:12 +02:00
Thomas Camlong 93bcc5945f fix: address PR review findings
- Revert undici to v7.27.1 (v8 blocked by #5467 qBittorrent regression)
- Broaden cache invalidation to utils.integration.invalidate() (covers
  all integration queries, not just integration.all)
- Add invalidation to onboarding integration create path
- Harden renovate test to assert allowBuilds key exists
- Remove deprecated Vitest 4 poolOptions config
- Fix CSS typo in docs menu transition (0.s → 0.5s)
- Update renovate.json5 comment to reference allowBuilds
2026-06-11 23:47:38 +02:00
Thomas Camlong d5776e7d2a fix: set verifyDepsBeforeRun to warn to prevent parallel install deadlock
pnpm v11 defaults to verifyDepsBeforeRun: install, which auto-runs
pnpm install before every pnpm run. When turbo runs multiple build
tasks in parallel, concurrent installs on the same workspace deadlock.
2026-06-11 22:46:16 +02:00
Thomas Camlong 0156fa0413 fix: bump pnpm to 11.5.3 and remove from mise (use corepack)
11.5.2 hit minimumReleaseAge constraints. Removed pnpm from .mise.toml
since corepack handles it via packageManager in package.json.
2026-06-11 22:36:08 +02:00
Thomas Camlong 38ce6edd17 chore(deps): upgrade to TypeScript 6, Vite 8, and @vitejs/plugin-react 6
- TypeScript 5.9 → 6.0: add types: ["node"] to base tsconfig, remove
  deprecated baseUrl where unused, add ignoreDeprecations for docs app,
  fix node_modules/ prefix imports with proper package exports
- Vite 7 → 8 + @vitejs/plugin-react 5 → 6: add vite as direct dep
- Add CSS module type declarations for TS6 compatibility
- Update renovate test for pnpm v11 allowBuilds migration
2026-06-11 22:28:21 +02:00
Thomas Camlong f1fb760c69 chore(deps): batch dependency updates, pnpm v11 migration, and fix integration cache
Batch-update dependencies from 17 open renovate PRs:
- @modelcontextprotocol/sdk 1.26→1.29, tiptap 3.25→3.26, ioredis 5.11.0→5.11.1
- mysql2 3.22.4→3.22.5, sql.js ^1.12→^1.14, @types/react 19.2.16→19.2.17
- oxlint ^1.67→^1.68, oxfmt ^0.53→^0.54, concurrently ^9→^10, mantine-contextmenu ^7→^9
- postcss ^8.4→^8.5.10 (security), @fec/remark-a11y-emoji ^3→^4

Migrate docs Tailwind CSS v3 → v4:
- Replace tailwindcss v3 + autoprefixer with tailwindcss v4 + @tailwindcss/postcss
- Convert config from tailwind.config.js to CSS @theme block
- Update PostCSS plugin in docusaurus.config.ts

Upgrade GitHub Actions:
- configure-pages v5→v6, upload-pages-artifact v4→v5, deploy-pages v4→v5
- EndBug/add-and-commit v9→v10, git-auto-commit-action v5→v7

Migrate pnpm v10 → v11 (unblocks #5679):
- Move package.json#pnpm config to pnpm-workspace.yaml
- Convert onlyBuiltDependencies/ignoredBuiltDependencies to allowBuilds map
- Migrate .npmrc settings (nodeLinker, strictPeerDependencies, engineStrict)
- Update packageManager to pnpm@11.5.2

Fix proxmox-api undici type incompatibility:
- Cast fetch parameter to resolve type mismatch between bundled undici versions

Fix integration cache not invalidating after create/update/delete:
- Add utils.integration.all.invalidate() to all mutation onSuccess handlers
- Previously only Next.js server cache was revalidated, not client tRPC cache
2026-06-11 22:14:03 +02:00
57 changed files with 2105 additions and 2779 deletions

No files matched your search

+2 -2
View File
@@ -22,7 +22,7 @@
automerge: true,
},
{
// disallow automerge for onlyBuiltDependencies
// disallow automerge for allowBuilds dependencies (native builds need manual verification)
matchPackageNames: [
"@tree-sitter-grammars/tree-sitter-yaml",
"bcrypt",
@@ -46,7 +46,7 @@
updateInternalDeps: true,
rangeStrategy: "bump",
automerge: false,
baseBranches: ["dev"],
baseBranchPatterns: ["dev"],
dependencyDashboard: false,
// prevent special characters in branch names (otherwise git errors occur on windows)
branchNameStrict: true,
+1 -1
View File
@@ -52,7 +52,7 @@ jobs:
xargs -r -a /tmp/changed-files pnpm exec oxfmt --write
- name: Commit fixes
uses: stefanzweifel/git-auto-commit-action@v5
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "style: auto-fix lint and format"
+3 -3
View File
@@ -38,9 +38,9 @@ jobs:
env:
CI: "true"
- name: Setup Pages
uses: actions/configure-pages@v5
uses: actions/configure-pages@v6
- name: Upload pages artifact
uses: actions/upload-pages-artifact@v4
uses: actions/upload-pages-artifact@v5
with:
path: "apps/docs/build"
@@ -53,4 +53,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5
@@ -31,7 +31,7 @@ jobs:
run: pnpm api-dump-contributors
- name: Commit updated contributors
uses: EndBug/add-and-commit@v9
uses: EndBug/add-and-commit@v10
with:
add: "apps/docs/static"
default_author: github_actor
+1 -1
View File
@@ -58,7 +58,7 @@ jobs:
echo "OpenAPI spec updated ($(jq '.paths | length' /tmp/openapi.json) paths)"
- name: Commit updated spec
uses: EndBug/add-and-commit@v9
uses: EndBug/add-and-commit@v10
with:
add: "apps/docs/static/api/"
default_author: github_actor
-1
View File
@@ -1,3 +1,2 @@
[tools]
node = "24.16.0"
pnpm = "10.34.1"
+2 -6
View File
@@ -1,6 +1,2 @@
# Expo doesn't play nice with pnpm by default.
# The symbolic links of pnpm break the rules of Expo monorepos.
# @link https://docs.expo.dev/guides/monorepos/#common-issues
node-linker=hoisted
strict-peer-dependencies=false
engine-strict=true
# pnpm v11: non-auth settings moved to pnpm-workspace.yaml
# Keep this file for any future registry/auth configuration
Binary file not shown.

Before

Width:  |  Height:  |  Size: 786 KiB

@@ -1,54 +0,0 @@
---
title: 'Beszel'
description: "Beszel is a lightweight server monitoring platform with Docker stats, historical data, and alert functions."
hide_title: true
---
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
import { beszelIntegration } from '.';
import { beszelSystemStatsWidget } from '@site/docs/widgets/beszel-system-stats';
import { beszelSystemGridWidget } from '@site/docs/widgets/beszel-system-grid';
import { beszelSystemTableWidget } from '@site/docs/widgets/beszel-system-table';
import { beszelAlertsWidget } from '@site/docs/widgets/beszel-alerts';
import beszelDashboard from './img/beszel-dashboard.png';
<IntegrationHeader
integration={beszelIntegration}
categories={['System Monitoring']}
/>
Beszel monitors your servers in real time via lightweight agents, collecting CPU, memory, disk, network, GPU, temperature, and Docker container metrics. It stores historical data and supports configurable alerts.
### Screenshots
<img src={beszelDashboard} alt="Beszel dashboard overview showing all four widgets" />
### Widgets & Capabilities
<IntegrationCapabilites
items={[
{ widget: beszelSystemStatsWidget },
{ widget: beszelSystemGridWidget },
{ widget: beszelSystemTableWidget },
{ widget: beszelAlertsWidget },
]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets secrets={[{
credentials: ['username', 'password'],
steps: [
'Open your Beszel hub instance (default port 8090)',
'Use the same credentials you use to log in to the Beszel web UI',
]
}]} />
:::tip
Beszel uses PocketBase under the hood. The default admin credentials are set during the initial Beszel setup.
:::
@@ -1,9 +0,0 @@
import { IntegrationDefinition } from "@site/src/types";
export const beszelIntegration: IntegrationDefinition = {
name: "Beszel",
description:
"Beszel is a lightweight server monitoring platform with Docker stats, historical data, and alert functions.",
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/beszel.svg",
path: "../../integrations/beszel",
};
@@ -1,100 +0,0 @@
---
title: Custom Widgets
tags:
- Management
- Widgets
- Custom
- API
---
Custom widgets let you create your own dashboard widgets that fetch data from any external API and render it with configurable displays. They are managed from the **Management → Custom Widgets** page (admin only).
:::note Beta
Custom widgets are currently in beta. Share feedback and report issues in the [GitHub discussion](https://github.com/homarr-labs/homarr/discussions/5916).
:::
## Overview
Custom widgets connect to any HTTP API endpoint and display the response data using one of several built-in display types. You can create multiple widget definitions, each with its own API configuration, display settings, and optional secrets for authentication.
## Creating a Custom Widget
Navigate to **Management → Custom Widgets** and click **New Custom Widget**. You'll need to configure:
### API Configuration
- **URL** — The full URL of the API endpoint to fetch
- **Method** — HTTP method (`GET`, `POST`, etc.)
- **Headers** — Custom HTTP headers (e.g. `Authorization`, `Content-Type`)
- **Body** — Request body for POST/PUT methods
- **Auth type** — Authentication method (`none`, `bearer`, `basic`, `API key in header`, `API key in query`)
### Secrets
Custom widgets support encrypted secret storage for API keys and passwords. Secrets are stored using AES-256-CBC encryption (same as integration secrets) and are **excluded from exports** for security.
### Display Configuration
Choose from the following display types:
| Display Type | Description |
|---|---|
| **Single Value** | Shows a single extracted value prominently |
| **Key-Value** | Displays data as labeled key-value pairs |
| **Table** | Renders data in a table layout |
| **Stat Grid** | Shows multiple statistics in a grid |
| **Progress Bars** | Visualizes values as progress bars |
| **Status Indicator** | Shows status with colored indicators |
| **Count Grid** | Displays count values in a grid layout |
| **Raw** | Shows the raw API response |
| **Action Button** | A button that triggers an API call when clicked |
| **Custom JSX** | Write custom JSX templates using whitelisted Mantine components |
:::tip Refresh interval
When adding a Custom API widget to a board, you can configure its **refresh interval** (1–3600 seconds, default 30s) in the widget's board options.
:::
## Custom JSX Display
The Custom JSX display type lets you write JSX templates to create rich, interactive displays. For security, only whitelisted components are allowed:
- **Mantine components** — `Text`, `Badge`, `Group`, `Stack`, `Paper`, `Progress`, `RingProgress`, `ThemeIcon`, `Anchor`, `Image`, `Divider`, `Code`, `Table`, and more
- **Interactive components** — `PaginatedList`, `TabsContainer`, `Collapsible`, `StatBar`, `TypeBadge`
Templates have access to the API response data and can use JSONPath expressions to extract values.
:::warning Security
The JSX sandbox blocks dangerous patterns like `eval`, `Function`, `__proto__`, and `javascript:` URLs. However, only admins can create custom widgets.
:::
## Live Preview
The widget editor includes a live preview panel that shows how the widget will look with real API data. Use the **Debug** panel to inspect the raw API response and test JSONPath expressions.
## Copy AI Prompt
Click the **Copy AI Prompt** button to copy a prompt containing the API's JSON Schema and sample response. Paste this into an LLM to get help writing JSONPath expressions or Custom JSX templates.
## Import & Export
Custom widget definitions can be exported as JSON files and imported on other Homarr instances:
- **Export** — Downloads a JSON file with the widget definition (secrets are excluded)
- **Import** — Upload a previously exported JSON file to create a new widget definition
- The export format uses `$schema: "homarr-custom-widget-v2"`
## Using Custom Widgets on a Board
Once created, custom widgets appear in the widget picker when editing a board. Each custom widget definition can be added to any board like a built-in widget.
## Seed Fixtures
Homarr ships with example custom widget definitions (disabled by default) to help you get started:
- **Currency Exchange (JPY)** — Live exchange rates
- **Random Dog Fact** — Random dog facts
- **Jellyfin library** — Jellyfin server statistics
Enable them from the custom widgets management page or use them as templates for your own widgets.
Additional examples like the Pokédex are available as importable JSON files in the repository under `packages/db/seed/custom-widgets/`.
@@ -1,5 +0,0 @@
{
"label": "Tools",
"collapsed": true,
"position": 10
}
@@ -1,66 +0,0 @@
---
title: SQLite Backup & Restore
tags:
- Management
- Backup
- Restore
- SQLite
---
The SQLite Backup & Restore feature allows you to export and import your entire Homarr database as a portable ZIP file. This is useful for creating backups, migrating between instances, or recovering from failures.
:::info SQLite only
This feature is only available when using the SQLite database driver (`DB_DRIVER=better-sqlite3`). MySQL and PostgreSQL users should use their respective database backup tools.
:::
## Accessing Backup & Restore
Navigate to **Management → Tools → Backup** from the sidebar. This page is only visible when running with SQLite.
## Exporting a Backup
Click the **Export** button to download a ZIP archive containing:
- **`db.sqlite`** — A consistent snapshot of your database (created via `VACUUM INTO`)
- **`metadata.json`** — Contains:
- `homarrVersion` — The version of Homarr that created the backup
- `exportedAt` — Timestamp of the export
- `dbDialect` — The database dialect (`sqlite`)
- `encryptionKey` — Your `SECRET_ENCRYPTION_KEY` (needed to decrypt integration secrets)
:::caution
The exported ZIP contains your encryption key and database. Store it securely and do not share it publicly.
:::
A confirmation dialog will ask you to type **"I understand"** before the export proceeds.
## Importing a Backup
1. Click the **Import** button and upload a previously exported ZIP file
2. Homarr analyzes the backup **entirely in your browser** using WebAssembly (sql.js) — no data is sent to the server during preview
3. The preview shows:
- **Entity counts** — How many boards, apps, users, integrations, etc. are in the backup
- **Board names** — A list of all boards contained in the backup
- **Migration status** — If the backup is from an older version, Homarr shows each pending database migration as animated timeline steps
4. Review the preview and type **"I understand"** to confirm
5. Homarr applies any pending migrations, re-encrypts integration secrets if the `SECRET_ENCRYPTION_KEY` differs between instances, and replaces the live database
6. The server automatically restarts to pick up the new database
:::tip
The import process is **backward-compatible** with older backups that used a separate `encryption-key.txt` file instead of embedding the key in `metadata.json`.
:::
## Onboarding Integration
When setting up a fresh Homarr instance with SQLite, the onboarding wizard includes a **"Restore from SQLite backup"** option. This lets you upload a backup ZIP to bootstrap your new instance with data from a previous installation.
## Docker Restart Behavior
After a successful import, only the Next.js process restarts — nginx and Redis remain running. The `run.sh` script automatically restarts Next.js in a loop, so the application comes back online within seconds. Running `docker stop` still works gracefully.
## Best Practices
- **Back up regularly** — Create exports before major updates or configuration changes
- **Test restores** — Periodically verify your backups by importing them on a test instance
- **Secure storage** — Keep backups in an encrypted or access-controlled location since they contain your encryption key
- **Check versions** — Backups from newer versions cannot be imported into older Homarr instances; always import into the same or newer version
Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

@@ -1,39 +0,0 @@
---
title: 'Beszel Alerts'
description: 'View Beszel alert configurations and history with triggered/ok status indicators.'
hide_title: true
---
import { WidgetHeader } from '@site/src/components/widgets/header';
import { AddingWidget } from '@site/src/components/widgets/adding';
import { WidgetConfig } from '@site/src/components/widgets/configuration';
import { WidgetIntegrations } from '@site/src/components/widgets/integrations';
import { beszelAlertsWidget } from '.';
import { beszelIntegration } from '@site/docs/integrations/beszel';
import beszelAlerts from './img/beszel-alerts.png';
<WidgetHeader
widget={beszelAlertsWidget}
categories={['System Monitoring', 'Alerts']}
/>
This widget displays your Beszel alert configurations separated by status (triggered and ok). It also shows alert history with timestamps. Alerts are updated in real time via WebSocket subscriptions.
### Screenshots
<img src={beszelAlerts} alt="Beszel Alerts widget showing triggered and ok alerts with history" />
### Supported Integrations
<WidgetIntegrations items={[{
integration: beszelIntegration,
note: "Displays alert configurations and trigger history"
}]} />
### Adding the widget
<AddingWidget />
### Configuration
<WidgetConfig configuration={beszelAlertsWidget.configuration} />
@@ -1,25 +0,0 @@
import { WidgetDefinition } from "@site/src/types";
import { IconBell } from "@tabler/icons-react";
export const beszelAlertsWidget: WidgetDefinition = {
icon: IconBell,
name: "Beszel Alerts",
description: "View Beszel alert configurations and history with triggered/ok status indicators.",
path: "../../widgets/beszel-alerts",
configuration: {
items: [
{
name: "Show history",
description: "Displays the alert trigger history",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Max history items",
description: "Maximum number of history entries to display (1-100)",
values: "Number (1-100)",
defaultValue: "10",
},
],
},
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

@@ -1,39 +0,0 @@
---
title: 'Beszel Systems (Grid)'
description: 'Card grid view of all Beszel-monitored systems with real-time metrics.'
hide_title: true
---
import { WidgetHeader } from '@site/src/components/widgets/header';
import { AddingWidget } from '@site/src/components/widgets/adding';
import { WidgetConfig } from '@site/src/components/widgets/configuration';
import { WidgetIntegrations } from '@site/src/components/widgets/integrations';
import { beszelSystemGridWidget } from '.';
import { beszelIntegration } from '@site/docs/integrations/beszel';
import beszelSystemGrid from './img/beszel-system-grid.png';
<WidgetHeader
widget={beszelSystemGridWidget}
categories={['System Monitoring']}
/>
This widget displays all your Beszel-monitored systems as a responsive card grid. Each card shows real-time metrics like CPU, memory, disk, network, temperature, and more. Cards adapt their layout based on available space.
### Screenshots
<img src={beszelSystemGrid} alt="Beszel System Grid widget showing system cards with metrics" />
### Supported Integrations
<WidgetIntegrations items={[{
integration: beszelIntegration,
note: "Displays all monitored systems as a card grid"
}]} />
### Adding the widget
<AddingWidget />
### Configuration
<WidgetConfig configuration={beszelSystemGridWidget.configuration} />
@@ -1,88 +0,0 @@
import { WidgetDefinition } from "@site/src/types";
import { IconLayoutGrid } from "@tabler/icons-react";
export const beszelSystemGridWidget: WidgetDefinition = {
icon: IconLayoutGrid,
name: "Beszel Systems (Grid)",
description: "Card grid view of all Beszel-monitored systems with real-time metrics.",
path: "../../widgets/beszel-system-grid",
configuration: {
items: [
{
name: "Status filter",
description: "Filter systems by their current status",
values: {
type: "select",
options: ["All Systems", "Up", "Down", "Paused"],
},
defaultValue: "All Systems",
},
{
name: "Show CPU",
description: "Displays CPU usage on each system card",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show memory",
description: "Displays memory usage on each system card",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show disk",
description: "Displays disk usage on each system card",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show GPU",
description: "Displays GPU usage on each system card",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show load average",
description: "Displays system load average",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show network",
description: "Displays network throughput",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show temperature",
description: "Displays system temperature",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show battery",
description: "Displays battery level (if available)",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show services",
description: "Displays running Docker services count",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show uptime",
description: "Displays system uptime",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show agent version",
description: "Displays the Beszel agent version",
values: { type: "boolean" },
defaultValue: "yes",
},
],
},
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 688 KiB

@@ -1,39 +0,0 @@
---
title: 'Beszel System Stats'
description: 'Time-series charts for CPU, memory, disk, network, and Docker container metrics from Beszel.'
hide_title: true
---
import { WidgetHeader } from '@site/src/components/widgets/header';
import { AddingWidget } from '@site/src/components/widgets/adding';
import { WidgetConfig } from '@site/src/components/widgets/configuration';
import { WidgetIntegrations } from '@site/src/components/widgets/integrations';
import { beszelSystemStatsWidget } from '.';
import { beszelIntegration } from '@site/docs/integrations/beszel';
import beszelSystemStats from './img/beszel-system-stats.png';
<WidgetHeader
widget={beszelSystemStatsWidget}
categories={['System Monitoring', 'Charts']}
/>
This widget displays time-series charts for a selected Beszel system, including CPU, memory, disk, network, and Docker container metrics. Data is cached and refreshed periodically. You can select a time period ranging from 1 hour to 30 days.
### Screenshots
<img src={beszelSystemStats} alt="Beszel System Stats widget showing CPU, memory, disk and network charts" />
### Supported Integrations
<WidgetIntegrations items={[{
integration: beszelIntegration,
note: "Displays time-series charts for a selected system"
}]} />
### Adding the widget
<AddingWidget />
### Configuration
<WidgetConfig configuration={beszelSystemStatsWidget.configuration} />
@@ -1,70 +0,0 @@
import { WidgetDefinition } from "@site/src/types";
import { IconChartAreaLine } from "@tabler/icons-react";
export const beszelSystemStatsWidget: WidgetDefinition = {
icon: IconChartAreaLine,
name: "Beszel System Stats",
description: "Time-series charts for CPU, memory, disk, network, and Docker container metrics from Beszel.",
path: "../../widgets/beszel-system-stats",
configuration: {
items: [
{
name: "Time period",
description: "The time range for chart data",
values: {
type: "select",
options: ["1 Hour", "12 Hours", "24 Hours", "1 Week", "30 Days"],
},
defaultValue: "1 Hour",
},
{
name: "Show CPU",
description: "Displays the CPU usage chart",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show memory",
description: "Displays the memory usage chart",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show disk",
description: "Displays the disk usage chart",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show disk I/O",
description: "Displays the disk I/O chart",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show network",
description: "Displays the network chart",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show Docker CPU",
description: "Displays Docker container CPU usage chart",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show Docker memory",
description: "Displays Docker container memory usage chart",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show Docker network",
description: "Displays Docker container network chart",
values: { type: "boolean" },
defaultValue: "yes",
},
],
},
};
@@ -1,34 +0,0 @@
---
title: 'Beszel Systems (Table)'
description: 'Table view of all Beszel-monitored systems with sortable columns and status indicators.'
hide_title: true
---
import { WidgetHeader } from '@site/src/components/widgets/header';
import { AddingWidget } from '@site/src/components/widgets/adding';
import { WidgetConfig } from '@site/src/components/widgets/configuration';
import { WidgetIntegrations } from '@site/src/components/widgets/integrations';
import { beszelSystemTableWidget } from '.';
import { beszelIntegration } from '@site/docs/integrations/beszel';
<WidgetHeader
widget={beszelSystemTableWidget}
categories={['System Monitoring']}
/>
This widget displays all your Beszel-monitored systems in a sortable data table. Each row shows system metrics like CPU, memory, disk, network, temperature, and more. Columns can be toggled on or off, and the table supports sorting by any column.
### Supported Integrations
<WidgetIntegrations items={[{
integration: beszelIntegration,
note: "Displays all monitored systems in a sortable table"
}]} />
### Adding the widget
<AddingWidget />
### Configuration
<WidgetConfig configuration={beszelSystemTableWidget.configuration} />
@@ -1,106 +0,0 @@
import { WidgetDefinition } from "@site/src/types";
import { IconTable } from "@tabler/icons-react";
export const beszelSystemTableWidget: WidgetDefinition = {
icon: IconTable,
name: "Beszel Systems (Table)",
description: "Table view of all Beszel-monitored systems with sortable columns and status indicators.",
path: "../../widgets/beszel-system-table",
configuration: {
items: [
{
name: "Status filter",
description: "Filter systems by their current status",
values: {
type: "select",
options: ["All Systems", "Up", "Down", "Paused"],
},
defaultValue: "All Systems",
},
{
name: "Sort by",
description: "Default column to sort the table by",
values: {
type: "select",
options: ["System", "CPU", "Memory", "Disk", "GPU", "Load Avg", "Net", "Temp", "Services", "Uptime", "Agent"],
},
defaultValue: "System",
},
{
name: "Sort direction",
description: "Default sort direction",
values: {
type: "select",
options: ["Ascending", "Descending"],
},
defaultValue: "Ascending",
},
{
name: "Show CPU",
description: "Displays the CPU column",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show memory",
description: "Displays the memory column",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show disk",
description: "Displays the disk column",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show GPU",
description: "Displays the GPU column",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show load average",
description: "Displays the load average column",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show network",
description: "Displays the network column",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show temperature",
description: "Displays the temperature column",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show battery",
description: "Displays the battery column",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show services",
description: "Displays the services column",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show uptime",
description: "Displays the uptime column",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show agent version",
description: "Displays the agent version column",
values: { type: "boolean" },
defaultValue: "yes",
},
],
},
};
@@ -1,29 +0,0 @@
---
title: 'Custom API'
description: 'Displays data from a custom API endpoint using configurable display types and optional JSX templates.'
hide_title: true
---
import { WidgetHeader } from '@site/src/components/widgets/header';
import { AddingWidget } from '@site/src/components/widgets/adding';
import { customApiWidget } from '.';
<WidgetHeader
widget={customApiWidget}
categories={['Custom', 'API']}
/>
This widget fetches data from a custom API endpoint defined in [Custom Widgets management](/docs/management/custom-widgets) and renders it using configurable display types.
Custom API widgets are created and configured through the admin management page. Each widget definition specifies the API endpoint, authentication, display type, and refresh interval.
See the [Custom Widgets documentation](/docs/management/custom-widgets) for full details on creating and configuring custom widgets.
### Adding the widget
<AddingWidget />
:::note
Custom API widgets must first be created in **Management → Custom Widgets** before they appear in the widget picker.
:::
@@ -1,9 +0,0 @@
import { WidgetDefinition } from "@site/src/types";
import { IconApi } from "@tabler/icons-react";
export const customApiWidget: WidgetDefinition = {
icon: IconApi,
name: "Custom API",
description: "Displays data from a custom API endpoint using configurable display types and optional JSX templates.",
path: "../../widgets/custom-api",
};
+2 -4
View File
@@ -285,13 +285,11 @@ const config: Config = {
};
},
"@signalwire/docusaurus-plugin-llms-txt",
async function tailwindCssPlugin(context, options) {
async function tailwindCssPlugin() {
return {
name: "docusaurus-tailwindcss",
configurePostCss(postcssOptions) {
// Appends TailwindCSS and AutoPrefixer.
postcssOptions.plugins.push(require("tailwindcss"));
postcssOptions.plugins.push(require("autoprefixer"));
postcssOptions.plugins.push(require("@tailwindcss/postcss"));
return postcssOptions;
},
};
+4 -4
View File
@@ -32,25 +32,25 @@
"@scalar/api-reference-react": "catalog:",
"@signalwire/docusaurus-plugin-llms-txt": "^1.2.2",
"@tabler/icons-react": "catalog:",
"autoprefixer": "^10.5.0",
"@tailwindcss/postcss": "^4.3.0",
"clsx": "catalog:",
"dayjs": "catalog:",
"docusaurus-plugin-image-zoom": "^3.0.1",
"leader-line-new": "^1.1.9",
"postcss": "^8.4.32",
"postcss": "^8.5.10",
"posthog-docusaurus": "^2.0.5",
"prism-react-renderer": "^2.4.1",
"random-word-slugs": "^0.1.7",
"react": "catalog:",
"react-dom": "catalog:",
"swiper": "^12.2.0",
"tailwindcss": "^3.4.19"
"tailwindcss": "^4.3.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.10.1",
"@docusaurus/tsconfig": "^3.10.1",
"@docusaurus/types": "^3.10.1",
"@fec/remark-a11y-emoji": "^3.1.0",
"@fec/remark-a11y-emoji": "^4.0.2",
"@tsconfig/docusaurus": "^2.0.9",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
+10 -4
View File
@@ -1,9 +1,15 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss/theme" layer(theme);
@import "tailwindcss/utilities" layer(utilities);
@import url(https://api.fontshare.com/v2/css?f[]=work-sans@2,1&display=swap);
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
@theme {
--spacing-128: 32rem;
--breakpoint-3xl: 1697px;
}
:root {
--ifm-color-primary: #fa5252;
--ifm-color-primary-dark: #f93232;
@@ -87,7 +93,7 @@
}
.menu__list {
transition: all 0.s ease-in-out !important;
transition: all 0.5s ease-in-out !important;
}
.announcementBar_node_modules-\@docusaurus-theme-classic-lib-theme-AnnouncementBar-styles-module {
-19
View File
@@ -1,19 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: {
spacing: {
128: "32rem",
},
screens: {
"3xl": "1697px",
},
},
},
darkMode: ["class", '[data-theme="dark"]'],
plugins: [],
corePlugins: {
preflight: false,
},
};
+1
View File
@@ -1,6 +1,7 @@
{
"extends": "@docusaurus/tsconfig",
"compilerOptions": {
"ignoreDeprecations": "6.0",
"baseUrl": ".",
"target": "es5",
"lib": [
@@ -19,6 +19,7 @@ export const DeleteIntegrationActionButton = ({ count, integration }: DeleteInte
const t = useScopedI18n("integration.page.delete");
const router = useRouter();
const { openConfirmModal } = useConfirmModal();
const utils = clientApi.useUtils();
const { mutateAsync, isPending } = clientApi.integration.delete.useMutation();
return (
@@ -43,6 +44,7 @@ export const DeleteIntegrationActionButton = ({ count, integration }: DeleteInte
router.replace("/manage/integrations");
}
void revalidatePathActionAsync("/manage/integrations");
void utils.integration.invalidate();
},
onError: () => {
showErrorNotification({
@@ -54,6 +54,7 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
const hasUrlSecret = initialSecretsKinds.includes("url");
const utils = clientApi.useUtils();
const router = useRouter();
const form = useZodForm(formSchema, {
initialValues: {
@@ -102,6 +103,7 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
title: t("integration.page.edit.notification.success.title"),
message: t("integration.page.edit.notification.success.message"),
});
void utils.integration.invalidate();
void revalidatePathActionAsync("/manage/integrations").then(() => router.push("/manage/integrations"));
},
onError: () => {
@@ -97,13 +97,19 @@ export const NewIntegrationForm = ({
},
});
const utils = clientApi.useUtils();
const { mutateAsync: createIntegrationAsync, isPending: isCreatePending } = clientApi.integration.create.useMutation({
async onSuccess() {
await revalidatePathActionAsync("/manage/integrations");
await utils.integration.invalidate();
},
});
const { mutateAsync: createOnboardingIntegrationAsync, isPending: isOnboardingCreatePending } =
clientApi.onboard.createIntegration.useMutation();
clientApi.onboard.createIntegration.useMutation({
async onSuccess() {
await utils.integration.invalidate();
},
});
const isPending = isCreatePending || isOnboardingCreatePending;
const [error, setError] = useState<null | AnyMappedTestConnectionError>(null);
@@ -2,7 +2,7 @@
import type { SegmentedControlItem } from "@mantine/core";
import { Button, Fieldset, Grid, Group, SegmentedControl, Stack, Textarea, TextInput } from "@mantine/core";
import { WidgetIntegrationSelect } from "node_modules/@homarr/widgets/src/widget-integration-select";
import { WidgetIntegrationSelect } from "@homarr/widgets/widget-integration-select";
import type { z } from "zod/v4";
import { clientApi } from "@homarr/api/client";
@@ -2,7 +2,7 @@ import { Badge, Card } from "@mantine/core";
import { useElementSize } from "@mantine/hooks";
import { QueryErrorResetBoundary } from "@tanstack/react-query";
import combineClasses from "clsx";
import { NoIntegrationSelectedError } from "node_modules/@homarr/widgets/src/errors";
import { NoIntegrationSelectedError } from "@homarr/widgets/errors/classes";
import { ErrorBoundary } from "react-error-boundary";
import { useRequiredBoard } from "@homarr/boards/context";
-1
View File
@@ -1,7 +1,6 @@
{
"extends": "@homarr/tsconfig/base.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"~/*": ["./src/*"],
"@homarr/node-unifi": ["../../node_modules/@types/node-unifi"]
+1 -2
View File
@@ -1,9 +1,8 @@
{
"extends": "@homarr/tsconfig/base.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"~/*": ["src/*"],
"~/*": ["./src/*"],
"@homarr/node-unifi": ["../../node_modules/@types/node-unifi"]
},
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
+1 -2
View File
@@ -1,9 +1,8 @@
{
"extends": "@homarr/tsconfig/base.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"~/*": ["src/*"],
"~/*": ["./src/*"],
"@homarr/node-unifi": ["../../node_modules/@types/node-unifi"]
},
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
+1
View File
@@ -2,5 +2,6 @@ FROM node:24.16.0-trixie AS base
WORKDIR /app
COPY package.json .
COPY pnpm-lock.yaml .
COPY pnpm-workspace.yaml .
RUN corepack enable pnpm && pnpm install --frozen-lockfile
CMD ["sleep", "60s"]
+2 -7
View File
@@ -6,12 +6,7 @@
},
"engines": {
"node": ">=24.16.0",
"pnpm": ">=10.34.1"
"pnpm": ">=11.0.0"
},
"packageManager": "pnpm@10.34.1",
"pnpm": {
"onlyBuiltDependencies": [
"better-sqlite3"
]
}
"packageManager": "pnpm@11.5.3"
}
@@ -0,0 +1,2 @@
allowBuilds:
better-sqlite3: true
+3 -47
View File
@@ -56,59 +56,15 @@
"testcontainers": "catalog:",
"turbo": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
"vite-tsconfig-paths": "catalog:",
"vitest": "catalog:"
},
"engines": {
"node": ">=24.16.0",
"pnpm": ">=10.34.1"
},
"packageManager": "pnpm@10.34.1",
"pnpm": {
"onlyBuiltDependencies": [
"@tree-sitter-grammars/tree-sitter-yaml",
"bcrypt",
"better-sqlite3",
"cpu-features",
"esbuild",
"sharp",
"ssh2",
"tree-sitter",
"tree-sitter-json"
],
"overrides": {
"@babel/helpers@<7.26.10": ">=7.29.7",
"@babel/runtime@<7.26.10": ">=7.29.7",
"axios@>=1.0.0 <1.8.2": ">=1.17.0",
"brace-expansion@>=2.0.0 <=2.0.1": ">=5.0.6",
"brace-expansion@>=1.0.0 <=1.1.11": ">=5.0.6",
"esbuild@<=0.24.2": ">=0.28.0",
"form-data@>=4.0.0 <4.0.4": ">=4.0.5",
"hono@<4.6.5": ">=4.12.23",
"linkifyjs@<4.3.2": ">=4.3.3",
"nanoid@>=4.0.0 <5.0.9": ">=5.1.11",
"prismjs@<1.30.0": ">=1.30.0",
"proxmox-api>undici": "7.27.1",
"react-is": "^19.2.7",
"rollup@>=4.0.0 <4.22.4": ">=4.61.1",
"sha.js@<=2.4.11": ">=2.4.12",
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.2",
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.2",
"tmp@<=0.2.3": ">=0.2.7",
"vite@>=5.0.0 <=5.4.18": ">=8.0.16"
},
"patchedDependencies": {
"@types/node-unifi": "patches/@types__node-unifi.patch",
"trpc-to-openapi": "patches/trpc-to-openapi.patch"
},
"allowUnusedPatches": true,
"ignoredBuiltDependencies": [
"@scarf/scarf",
"@swc/core",
"core-js-pure",
"protobufjs"
]
"pnpm": ">=11.0.0"
},
"packageManager": "pnpm@11.5.3",
"dependencies": {
"jsonpath-plus": "catalog:"
}
@@ -91,8 +91,8 @@ export type HomarrDocumentationPath =
| "/docs/tags/redis"
| "/docs/tags/responsive"
| "/docs/tags/roles"
| "/docs/tags/search"
| "/docs/tags/search-engines"
| "/docs/tags/search"
| "/docs/tags/secrets"
| "/docs/tags/security"
| "/docs/tags/self-signed"
@@ -126,8 +126,8 @@ export type HomarrDocumentationPath =
| "/docs/category/community"
| "/docs/category/developer-guides"
| "/docs/category/getting-started"
| "/docs/category/installation"
| "/docs/category/installation-1"
| "/docs/category/installation"
| "/docs/category/integrations"
| "/docs/category/management"
| "/docs/category/widgets"
@@ -317,7 +317,6 @@ export type HomarrDocumentationPath =
| "/docs/integrations/navidrome"
| "/docs/integrations/umami"
| "/docs/integrations/peanut"
| "/docs/integrations/beszel"
| "/docs/widgets/clock"
| "/docs/widgets/weather"
| "/docs/widgets/app"
@@ -359,9 +358,4 @@ export type HomarrDocumentationPath =
| "/docs/widgets/uptime-kuma"
| "/docs/widgets/audio-stats"
| "/docs/widgets/umami"
| "/docs/widgets/ups"
| "/docs/widgets/beszel-system-table"
| "/docs/widgets/beszel-system-grid"
| "/docs/widgets/beszel-alerts"
| "/docs/widgets/beszel-system-stats"
| "/docs/widgets/custom-api";
| "/docs/widgets/ups";
@@ -55,6 +55,6 @@ export const integrationDocSlugs: Record<IntegrationKind, string | null> = {
navidrome: "navidrome",
umami: "umami",
peaNut: "peanut",
beszel: "beszel",
beszel: null,
mock: null,
};
@@ -44,9 +44,9 @@ export const widgetDocSlugs: Record<WidgetKind, string | null> = {
audioStats: "audio-stats",
umami: "umami",
ups: "ups",
beszelSystemTable: "beszel-system-table",
beszelSystemGrid: "beszel-system-grid",
beszelAlerts: "beszel-alerts",
beszelSystemStats: "beszel-system-stats",
customApi: "custom-api",
beszelSystemTable: null,
beszelSystemGrid: null,
beszelAlerts: null,
beszelSystemStats: null,
customApi: null,
};
+1 -1
View File
@@ -479,7 +479,7 @@ export const integrationDefs = {
secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/beszel.svg",
category: ["healthMonitoring"],
documentationUrl: createDocumentationLink("/docs/integrations/beszel"),
documentationUrl: null,
defaultPort: 8090,
},
// This integration only returns mock data, it is used during development (but can also be used in production by directly going to the create page)
@@ -55,7 +55,8 @@ export class ProxmoxIntegration extends Integration implements IClusterHealthMon
host: this.url("/").host,
tokenID: `${this.getSecretValue("username")}@${this.getSecretValue("realm")}!${this.getSecretValue("tokenId")}`,
tokenSecret: this.getSecretValue("apiKey"),
fetch: fetchAsync,
// proxmox-api bundles its own undici types which are incompatible with the project's undici version
fetch: fetchAsync as any,
});
}
}
@@ -1,5 +1,4 @@
import proxmoxApi from "proxmox-api";
import type { fetch as undiciFetch } from "undici";
import { Response } from "undici";
import { describe, expect, test } from "vitest";
@@ -9,48 +8,32 @@ import { ProxmoxApiErrorHandler } from "../proxmox-error-handler";
describe("ProxmoxApiErrorHandler handleError should handle the provided error accordingly", () => {
test.each([400, 401, 500])("should handle %s error", async (statusCode) => {
// Arrange
// eslint-disable-next-line no-restricted-syntax
const mockedFetch: typeof undiciFetch = async () => {
return Promise.resolve(createFakeResponse(statusCode));
};
const mockedFetch = async () => Promise.resolve(createFakeResponse(statusCode));
// Act
const result = await runWithAsync(mockedFetch);
// Assert
expect(result).toBeInstanceOf(IntegrationResponseError);
const error = result as unknown as IntegrationResponseError;
expect(error.cause.statusCode).toBe(statusCode);
});
test("should handle other non successful status codes", async () => {
// Arrange
// eslint-disable-next-line no-restricted-syntax
const mockedFetch: typeof undiciFetch = async () => {
return Promise.resolve(createFakeResponse(404));
};
const mockedFetch = async () => Promise.resolve(createFakeResponse(404));
// Act
const result = await runWithAsync(mockedFetch);
// Assert
expect(result).toBeInstanceOf(IntegrationResponseError);
const error = result as unknown as IntegrationResponseError;
expect(error.cause.statusCode).toBe(404);
});
test("should handle request error", async () => {
// Arrange
const mockedFetch: typeof undiciFetch = () => {
const mockedFetch = () => {
const errorWithCode = new Error("Inner error") as Error & { code: string };
errorWithCode.code = "ECONNREFUSED";
throw new TypeError("Outer error", { cause: errorWithCode });
};
// Act
const result = await runWithAsync(mockedFetch);
// Assert
// In the end in should have the structure IntegrationRequestError -> RequestError -> TypeError -> Error (with code)
expect(result).toBeInstanceOf(IntegrationRequestError);
const error = result as unknown as IntegrationRequestError;
expect(error.cause.cause).toBeInstanceOf(TypeError);
@@ -65,13 +48,11 @@ describe("ProxmoxApiErrorHandler handleError should handle the provided error ac
const createFakeResponse = (statusCode: number) => {
return new Response(JSON.stringify({ data: {} }), {
status: statusCode,
// It expects a content-type and valid json response
// https://github.com/UrielCh/proxmox-api/blob/master/api/src/ProxmoxEngine.ts#L258
headers: { "content-type": "application/json;charset=UTF-8" },
});
};
const runWithAsync = async (mockedFetch: typeof undiciFetch) => {
const runWithAsync = async (mockedFetch: (...args: any[]) => any) => {
const integration = { id: "test", name: "test", url: "http://proxmox.example.com" };
const client = createProxmoxClient(mockedFetch);
const handler = new ProxmoxApiErrorHandler();
@@ -79,11 +60,11 @@ const runWithAsync = async (mockedFetch: typeof undiciFetch) => {
return await client.nodes.$get().catch((error) => handler.handleError(error, integration));
};
const createProxmoxClient = (fetch: typeof undiciFetch) => {
const createProxmoxClient = (fetch: (...args: any[]) => any) => {
return proxmoxApi({
host: "proxmox.example.com",
tokenID: "username@realm!tokenId",
tokenSecret: crypto.randomUUID(),
fetch,
fetch: fetch as any,
});
};
@@ -0,0 +1,2 @@
declare const classes: Record<string, string>;
export default classes;
@@ -0,0 +1,2 @@
declare const classes: Record<string, string>;
export default classes;
+2
View File
@@ -17,6 +17,8 @@
"./custom-api/component": "./src/custom-api/component.tsx",
"./custom-api/extract-display-data": "./src/custom-api/extract-display-data.ts",
"./errors": "./src/errors/component.tsx",
"./errors/classes": "./src/errors/index.ts",
"./widget-integration-select": "./src/widget-integration-select.tsx",
"./modals": "./src/modals/index.ts",
"./prefetch": "./src/prefetch.ts"
},
+1944 -1876
View File
File diff suppressed because it is too large. Load diff
+74 -28
View File
@@ -21,7 +21,7 @@ catalog:
"@immich/sdk": ^2.7.5
"@jellyfin/sdk": ^0.13.0
"@kubernetes/client-node": ^1.4.0
"@modelcontextprotocol/sdk": 1.26.0
"@modelcontextprotocol/sdk": 1.29.0
"@mantine/charts": ^9.3.0
"@mantine/colors-generator": ^9.3.0
"@mantine/core": ^9.3.0
@@ -52,22 +52,22 @@ catalog:
"@testcontainers/mysql": ^12.0.1
"@testcontainers/postgresql": ^12.0.1
"@testcontainers/redis": ^12.0.1
"@tiptap/extension-color": 3.25.0
"@tiptap/extension-highlight": 3.25.0
"@tiptap/extension-image": 3.25.0
"@tiptap/extension-link": ^3.25.0
"@tiptap/extension-placeholder": ^3.25.0
"@tiptap/extension-table": 3.25.0
"@tiptap/extension-table-cell": 3.25.0
"@tiptap/extension-table-header": 3.25.0
"@tiptap/extension-table-row": 3.25.0
"@tiptap/extension-task-item": 3.25.0
"@tiptap/extension-task-list": 3.25.0
"@tiptap/extension-text-align": 3.25.0
"@tiptap/extension-text-style": 3.25.0
"@tiptap/extension-underline": 3.25.0
"@tiptap/react": ^3.25.0
"@tiptap/starter-kit": ^3.25.0
"@tiptap/extension-color": 3.26.0
"@tiptap/extension-highlight": 3.26.0
"@tiptap/extension-image": 3.26.0
"@tiptap/extension-link": ^3.26.0
"@tiptap/extension-placeholder": ^3.26.0
"@tiptap/extension-table": 3.26.0
"@tiptap/extension-table-cell": 3.26.0
"@tiptap/extension-table-header": 3.26.0
"@tiptap/extension-table-row": 3.26.0
"@tiptap/extension-task-item": 3.26.0
"@tiptap/extension-task-list": 3.26.0
"@tiptap/extension-text-align": 3.26.0
"@tiptap/extension-text-style": 3.26.0
"@tiptap/extension-underline": 3.26.0
"@tiptap/react": ^3.26.0
"@tiptap/starter-kit": ^3.26.0
"@trpc/client": ^11.17.0
"@trpc/next": ^11.17.0
"@trpc/react-query": ^11.17.0
@@ -86,14 +86,14 @@ catalog:
"@types/node-unifi": ^2.5.1
"@types/pg": ^8.20.0
"@types/prismjs": ^1.26.6
"@types/react": 19.2.16
"@types/react": 19.2.17
"@types/react-dom": 19.2.3
"@scalar/api-reference-react": ^0.9.42
"@types/video.js": ^7.3.58
"@types/ws": ^8.18.1
"@types/xml2js": ^0.4.14
"@umami/node": ^0.4.0
"@vitejs/plugin-react": ^5.2.0
"@vitejs/plugin-react": ^6.0.2
"@vitest/coverage-v8": ^4.1.8
"@vitest/ui": ^4.1.8
"@xterm/addon-canvas": ^0.7.0
@@ -101,13 +101,13 @@ catalog:
"@xterm/xterm": ^6.0.0
adm-zip: 0.5.17
jszip: ^3.10.1
sql.js: ^1.12.0
sql.js: ^1.14.1
babel-plugin-react-compiler: ^1.0.0
bcrypt: ^6.0.0
better-sqlite3: ^12.10.0
chroma-js: ^3.2.0
clsx: ^2.1.1
concurrently: ^9.2.1
concurrently: ^10.0.3
conventional-changelog-conventionalcommits: ^9.3.1
cookie: 1.1.1
cookies: ^0.9.1
@@ -129,7 +129,7 @@ catalog:
flag-icons: ^7.5.0
glob: ^13.0.6
ical.js: ^2.2.1
ioredis: 5.11.0
ioredis: 5.11.1
isomorphic-dompurify: ^3.16.0
jotai: ^2.20.0
jsdom: ^29.1.1
@@ -138,19 +138,19 @@ catalog:
ldapts: 8.1.8
lodash.clonedeep: ^4.5.0
lucide-react: ^1.17.0
"mantine-contextmenu": ^7.17.1
"mantine-contextmenu": ^9.2.1
mantine-datatable: ^9.2.2
mantine-react-table: npm:mantine-react-table-open@9.0.3
mcp-handler: ^1.1.0
maria2: ^0.4.1
mysql2: 3.22.4
mysql2: 3.22.5
next: 16.2.7
next-auth: 5.0.0-beta.31
next-intl: 4.13.0
node-loader: ^2.1.0
octokit: ^5.0.5
oxfmt: ^0.53.0
oxlint: ^1.67.0
oxfmt: ^0.54.0
oxlint: ^1.68.0
pg: ^8.21.0
postcss-preset-mantine: ^1.18.0
prismjs: ^1.30.0
@@ -175,8 +175,9 @@ catalog:
tsdav: ^2.2.2
tsx: 4.20.4
turbo: ^2.9.16
typescript: ^5.9.3
undici: 8.4.0
typescript: ^6.0.3
vite: ^8.0.16
undici: 7.27.1
use-deep-compare-effect: ^1.8.1
video.js: ^8.23.7
vite-tsconfig-paths: ^6.1.1
@@ -189,9 +190,54 @@ catalog:
zod-form-data: ^3.0.2
zod-validation-error: ^5.0.0
catalogMode: strict
nodeLinker: hoisted
strictPeerDependencies: false
engineStrict: true
verifyDepsBeforeRun: warn
minimumReleaseAge: 4320
minimumReleaseAgeExclude:
- next
- "@next/*"
# Renovate security update: turbo@2.9.14
- turbo@2.9.14
overrides:
"@babel/helpers@<7.26.10": ">=7.29.7"
"@babel/runtime@<7.26.10": ">=7.29.7"
"axios@>=1.0.0 <1.8.2": ">=1.17.0"
"brace-expansion@>=2.0.0 <=2.0.1": ">=5.0.6"
"brace-expansion@>=1.0.0 <=1.1.11": ">=5.0.6"
"esbuild@<=0.24.2": ">=0.28.0"
"form-data@>=4.0.0 <4.0.4": ">=4.0.5"
"hono@<4.6.5": ">=4.12.23"
"linkifyjs@<4.3.2": ">=4.3.3"
"nanoid@>=4.0.0 <5.0.9": ">=5.1.11"
"prismjs@<1.30.0": ">=1.30.0"
"proxmox-api>undici": "7.27.1"
"react-is": "^19.2.7"
"rollup@>=4.0.0 <4.22.4": ">=4.61.1"
"sha.js@<=2.4.11": ">=2.4.12"
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.2"
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.2"
"tmp@<=0.2.3": ">=0.2.7"
"vite@>=5.0.0 <=5.4.18": ">=8.0.16"
patchedDependencies:
"@types/node-unifi": "patches/@types__node-unifi.patch"
"trpc-to-openapi": "patches/trpc-to-openapi.patch"
allowUnusedPatches: true
allowBuilds:
"@tree-sitter-grammars/tree-sitter-yaml": true
bcrypt: true
better-sqlite3: true
cpu-features: true
esbuild: true
sharp: true
ssh2: true
tree-sitter: true
tree-sitter-json: true
"@scarf/scarf": false
"@swc/core": false
core-js: false
core-js-pure: false
msw: false
protobufjs: false
vue-demi: false
+11 -5
View File
@@ -1,22 +1,28 @@
import fs from "fs/promises";
import { join } from "path";
import json5 from "json5";
import { parse } from "yaml";
import { describe, test } from "vitest";
describe("Renovate configuration tests", () => {
test("automerge should be disabled for onlyBuiltDependencies", async () => {
const packageJson = await import("../package.json");
test("automerge should be disabled for allowBuilds dependencies", async () => {
const workspaceYaml = await fs.readFile(join(__dirname, "../pnpm-workspace.yaml"), "utf-8").then(parse);
const renovateConfig = await fs.readFile(join(__dirname, "../.github/renovate.json5"), "utf-8").then(json5.parse);
const onlyBuiltDependencies = packageJson.pnpm.onlyBuiltDependencies;
if (!workspaceYaml.allowBuilds || typeof workspaceYaml.allowBuilds !== "object") {
throw new Error("pnpm-workspace.yaml must define allowBuilds as an object");
}
const allowedBuilds = Object.entries(workspaceYaml.allowBuilds)
.filter(([, allowed]) => allowed === true)
.map(([name]) => name);
const automergeDisabledDeps = renovateConfig.packageRules
.filter((rule: any) => rule.automerge === false)
.flatMap((rule: any) => rule.matchPackageNames || []);
const missingDeps = onlyBuiltDependencies.filter((dep: string) => !automergeDisabledDeps.includes(dep));
const missingDeps = allowedBuilds.filter((dep: string) => !automergeDisabledDeps.includes(dep));
if (missingDeps.length > 0) {
throw new Error(
`The following onlyBuiltDependencies are missing automerge disable rules in renovate.json5: ${missingDeps.join(", ")}`,
`The following allowBuilds dependencies are missing automerge disable rules in renovate.json5: ${missingDeps.join(", ")}`,
);
}
});
+2 -3
View File
@@ -19,10 +19,9 @@
"noUncheckedIndexedAccess": true,
"strictNullChecks": true,
"experimentalDecorators": true,
"baseUrl": ".",
"types": ["node"],
"paths": {
"@homarr/node-unifi": ["${configDir}/../../node_modules/@types/node-unifi"],
"*": ["node_modules/*"]
"@homarr/node-unifi": ["${configDir}/../../node_modules/@types/node-unifi"]
}
},
"exclude": ["node_modules", "build", "dist", ".next"]
-5
View File
@@ -9,11 +9,6 @@ export default defineConfig({
environment: "jsdom",
include: ["**/*.spec.ts"],
clearMocks: true,
poolOptions: {
threads: {
singleThread: false,
},
},
coverage: {
provider: "v8",
reporter: ["html", "json-summary", "json"],