Compare commits

..
Author SHA1 Message Date
ajnart 6c3b584bd2 style: auto-fix lint and format 2026-07-21 08:08:53 +00:00
ajnart e3cf680cbd fix(integrations): support UniFi controller default ports 2026-07-21 10:07:25 +02:00
ajnart 24989ddd16 style: auto-fix lint and format 2026-07-06 00:07:03 +00:00
ajnart 8cd9e8c826 fix(integrations): use Unifi default port 8443 when no port is given
The node-unifi library hardcodes https:// in its base URL and the
Unifi controller API only speaks HTTPS. When a user adds the
integration with a bare http:// or https:// URL, getPortFromUrl
returned 80 or 443, and the library attempted a TLS handshake on
the wrong port. The server's plain HTTP response caused
'EPROTO ... wrong version number'.

Derive the port from the user's explicit port, or fall back to 8443
(the integration's defaultPort). Closes #6207.
2026-07-06 02:05:44 +02:00
45 changed files with 1096 additions and 924 deletions

No files matched your search

-1
View File
@@ -33,7 +33,6 @@ body:
options:
# The below comment is used to insert a new version with on-release.yml
#NEXT_VERSION#
- 1.69.2
- 1.69.0
- 1.68.0
- 1.67.0
+1 -1
View File
@@ -11,7 +11,7 @@ outputs:
runs:
using: "composite"
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up Docker Buildx
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
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
uses: actions/checkout@v6
- name: Obtain token
id: obtainToken
uses: actions/create-github-app-token@v3
+69 -7
View File
@@ -31,7 +31,7 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ github.head_ref }}
@@ -60,7 +60,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -78,7 +78,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -92,7 +92,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -106,7 +106,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Setup
uses: ./tooling/github/setup
@@ -124,7 +124,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Setup
uses: ./tooling/github/setup
- name: Set up Docker Buildx
@@ -153,7 +153,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup
@@ -163,3 +163,65 @@ jobs:
run: cp .env.example .env
- name: Build
run: pnpm turbo build --affected
memory-regression:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Build Docker image
uses: docker/build-push-action@v7
with:
platforms: linux/amd64
context: .
push: false
load: true
tags: homarr-memory-test
network: host
cache-from: type=gha
cache-to: type=gha,mode=max
env:
SKIP_ENV_VALIDATION: true
- name: Start container and measure memory
run: |
docker run -d --name homarr-mem-test -p 17575:7575 homarr-memory-test
# Wait for container to be healthy (up to 60s)
healthy=false
for i in $(seq 1 30); do
if curl -sf http://localhost:17575/api/health 2>/dev/null; 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 homarr-mem-test --tail 50
exit 1
fi
# Warm up: hit the dashboard page a few times
for i in $(seq 1 5); do
curl -sf http://localhost:17575/ > /dev/null 2>&1 || true
sleep 2
done
# Measure RSS after warmup
MEM_KB=$(docker stats --no-stream --format '{{.MemUsage}}' homarr-mem-test | sed 's| /.*||')
echo "Raw memory: $MEM_KB"
# Convert to numeric MB (handles GiB and MiB suffixes)
MEM_NUM=$(echo "$MEM_KB" | awk '{
if ($0 ~ /GiB/) { gsub(/GiB/,""); print $1 * 1024 }
else if ($0 ~ /MiB/) { gsub(/MiB/,""); print $1 }
else if ($0 ~ /KiB/) { gsub(/KiB/,""); print $1 / 1024 }
else { print $1 / 1048576 }
}')
echo "Memory usage: ${MEM_NUM} MB"
MAX_MB=700
if awk "BEGIN {exit !($MEM_NUM > $MAX_MB)}"; then
echo "ERROR: Memory usage ${MEM_NUM} MB exceeds ${MAX_MB} MB threshold"
docker logs homarr-mem-test --tail 50
exit 1
fi
echo "Memory check passed: ${MEM_NUM} MB <= ${MAX_MB} MB"
@@ -15,7 +15,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Obtain token
id: obtainToken
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Upload Crowdin translations
uses: crowdin/github-action@v2
@@ -26,7 +26,7 @@ jobs:
with:
args: "Beta release has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Get Next Version
@@ -57,7 +57,7 @@ jobs:
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # required to commit package.json & changelog changes, merge them to dev / next and publish the release
- uses: actions/checkout@v7
- uses: actions/checkout@v6
if: env.SKIP_RELEASE == 'false'
with:
persist-credentials: false
@@ -119,7 +119,7 @@ jobs:
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
ref: ${{ needs.release.outputs.git_ref }}
@@ -158,7 +158,7 @@ jobs:
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
ref: ${{ needs.release.outputs.git_ref }}
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Setup
uses: ./tooling/github/setup
- name: Build docs
@@ -29,7 +29,7 @@ jobs:
with:
args: "Automatic release has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Get Next Version
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Setup
uses: ./tooling/github/setup
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
ref: dev
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
ref: dev
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Validate prebuilt dependencies for amd64
id: validate-amd64
uses: ./.github/actions/prebuilt-debian
@@ -30,7 +30,7 @@ jobs:
runs-on: ubuntu-24.04-arm
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Validate prebuilt dependencies for arm64
id: validate-arm64
uses: ./.github/actions/prebuilt-debian
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- run: |
npx --yes --package renovate -- \
renovate-config-validator --strict .github/renovate.json5
+1 -1
View File
@@ -52,7 +52,7 @@ jobs:
permission-contents: write # required to commit to branch
permission-pull-requests: write # required to create pr & enable automerge
- name: Checkout code
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
token: ${{ steps.obtainToken.outputs.token }}
ref: dev
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
permission-pull-requests: write # required to create pr & enable automerge
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
@@ -28,7 +28,7 @@ jobs:
permission-contents: write # required to commit to branch
permission-pull-requests: write # required to create pr & enable automerge
- name: Checkout code
uses: actions/checkout@v7
uses: actions/checkout@v6
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
- name: Setup
-6
View File
@@ -1,9 +1,3 @@
## [1.69.2](https://github.com/homarr-labs/homarr/compare/v1.69.1...v1.69.2) (2026-07-06)
### Bug Fixes
* **layout:** raise AppShellHeader z-index above Indicator dots ([687edb9](https://github.com/homarr-labs/homarr/commit/687edb9226307cb83986097abb24fe7f672e1778)), closes [#6194](https://github.com/homarr-labs/homarr/issues/6194)
## [1.69.1](https://github.com/homarr-labs/homarr/compare/v1.69.0...v1.69.1) (2026-07-04)
### Bug Fixes
+1 -1
View File
@@ -58,7 +58,7 @@ import { immichServerStatsWidget } from '../../widgets/immich-server-stats';
Additional permissions may be required depending on your Immich version. If you encounter authorization errors, grant any missing permissions requested by the server.
</span>
),
),Ï
'Create the integration in Homarr and copy the API key to there'
]
}]} />
@@ -1,40 +1,48 @@
---
title: 'Unifi Controller'
title: "Unifi Controller"
description: "Unifi Controller is a network management platform for Unifi devices."
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 { 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 { IconTopologyFull } from "@tabler/icons-react";
import { unifiControllerIntegration } from '.';
import { networkControllerSummaryWidget } from '@site/docs/widgets/network-controller-summary';
import { networkControllerStatusWidget } from '@site/docs/widgets/network-controller-status';
import { unifiControllerIntegration } from ".";
import { networkControllerSummaryWidget } from "@site/docs/widgets/network-controller-summary";
import { networkControllerStatusWidget } from "@site/docs/widgets/network-controller-status";
<IntegrationHeader
integration={unifiControllerIntegration}
categories={['Network controller']}
/>
<IntegrationHeader integration={unifiControllerIntegration} categories={["Network controller"]} />
This integration is using the [node-unifi](https://www.npmjs.com/package/node-unifi) library under the hood to connect to the Unifi Controller.
Therfore it should support CloudKey Gen1, CloudKey Gen2, UnifiOS-based UDM-Pro controllers as well as self-hosted UniFi controllers.
### Widgets & Capabilities
<IntegrationCapabilites
items={[
{ widget: networkControllerSummaryWidget },
{ widget: networkControllerStatusWidget}
]}
items={[{ widget: networkControllerSummaryWidget }, { widget: networkControllerStatusWidget }]}
/>
### Adding the integration
<AddingIntegration />
Use the local HTTPS address of your controller:
- UniFi OS consoles such as Dream Router, Dream Machine, Cloud Gateway, and CloudKey use `https://<host>` (port 443).
- Self-hosted UniFi Network Servers normally use `https://<host>:8443`.
- If you omit the port, Homarr tries port 443 first and then port 8443 when the first port cannot be reached.
Use a local administrator account. Cloud-only accounts and accounts with multi-factor authentication are not supported by the underlying client.
### Secrets
<IntegrationSecrets secrets={[{
credentials: ['username', 'password'],
steps: []
}]} />
<IntegrationSecrets
secrets={[
{
credentials: ["username", "password"],
steps: [],
},
]}
/>
+1 -1
View File
@@ -43,7 +43,7 @@
"random-word-slugs": "^0.1.7",
"react": "catalog:",
"react-dom": "catalog:",
"swiper": "^14.0.1",
"swiper": "^12.2.0",
"tailwindcss": "^4.3.2"
},
"devDependencies": {
+2 -1
View File
@@ -1,7 +1,8 @@
@import url("https://api.fontshare.com/v2/css?f[]=work-sans@2,1&display=swap");
@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 {
+4 -5
View File
@@ -1,6 +1,9 @@
{
"extends": "@docusaurus/tsconfig",
"compilerOptions": {
"target": "ES2022",
"ignoreDeprecations": "6.0",
"baseUrl": ".",
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
@@ -9,14 +12,10 @@
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"paths": {
"@site/*": ["./*"]
},
},
"include": [
"**/*.ts",
+14 -12
View File
@@ -5,9 +5,21 @@ import "@homarr/common/env";
import "@homarr/core/infrastructure/logs/env";
import "@homarr/docker/env";
import { readFileSync } from "fs";
import type { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin";
const homarrVersion = (() => {
for (const candidate of ["../../package.json", "./package.json"]) {
try {
const pkg = JSON.parse(readFileSync(candidate, "utf-8"));
if (pkg.name === "homarr") return pkg.version as string;
} catch {}
}
return "unknown";
})();
// Package path does not work... so we need to use relative path
const withNextIntl = createNextIntlPlugin({
requestConfig: "../../packages/translation/src/request.ts",
@@ -15,7 +27,7 @@ const withNextIntl = createNextIntlPlugin({
const nextConfig: NextConfig = {
env: {
HOMARR_VERSION: process.env.HOMARR_VERSION ?? "unknown",
HOMARR_VERSION: homarrVersion,
},
output: "standalone",
reactStrictMode: true,
@@ -31,19 +43,9 @@ const nextConfig: NextConfig = {
experimental: {
optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"],
turbopackFileSystemCacheForDev: true,
preloadEntriesOnStart: false,
webpackMemoryOptimizations: true,
},
turbopack: {
// ponytail: known Turbopack NFT warning from path.join(process.cwd(), …) in
// src/app/api/backup/{route,shared}.ts. No working placement in 16.2.x
// (see vercel/next.js#95125). Suppress until upstream fix lands.
ignoreIssue: [
{
path: "**/*",
title: "Encountered unexpected file in NFT list",
},
],
},
transpilePackages: ["@homarr/ui", "@homarr/notifications", "@homarr/modals", "@homarr/spotlight", "@homarr/widgets"],
images: {
localPatterns: [
+1 -1
View File
@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"scripts": {
"build": "HOMARR_VERSION=$(node -p \"require('../../package.json').version\") pnpm with-env next build",
"build": "pnpm with-env next build",
"clean": "git clean -xdf .next .turbo node_modules",
"dev": "pnpm with-env next dev",
"format": "oxfmt .",
+8 -10
View File
@@ -1,13 +1,11 @@
import fs from "fs";
import path from "path";
export const findMigrationsFolder = () => {
const cwd = process.cwd();
const migrationsFolderCandidates = [
path.join(cwd, "db/migrations/sqlite"),
path.join(cwd, "packages/db/migrations/sqlite"),
path.resolve(cwd, "../../db/migrations/sqlite"),
"/app/db/migrations/sqlite",
];
return migrationsFolderCandidates.find((candidate) => fs.existsSync(candidate));
};
const migrationsFolderCandidates = [
path.join(process.cwd(), "db/migrations/sqlite"),
path.join(process.cwd(), "packages/db/migrations/sqlite"),
path.resolve(process.cwd(), "../../db/migrations/sqlite"),
"/app/db/migrations/sqlite",
];
export const findMigrationsFolder = () => migrationsFolderCandidates.find((candidate) => fs.existsSync(candidate));
+1 -1
View File
@@ -18,7 +18,7 @@ interface Props {
export const MainHeader = ({ logo, actions, hasNavigation = true }: Props) => {
return (
<AppShellHeader maw="100vw" zIndex={201} style={{ overflowX: "hidden" }}>
<AppShellHeader maw="100vw" style={{ overflowX: "hidden" }}>
<Group h="100%" gap="xl" px="md" justify="apart" wrap="nowrap">
<Group h="100%" align="center" style={{ flex: 1 }} wrap="nowrap">
{hasNavigation && <ClientBurger />}
@@ -6,5 +6,5 @@ import { DockerTable } from "~/app/[locale]/manage/tools/docker/docker-table";
export const DockerQuickAccessModal = createModal<void>(() => <DockerTable />).withOptions({
defaultTitle: (t) => t("docker.title"),
fullScreen: true,
size: 1400,
});
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "homarr-prebuilt-debian",
"private": true,
"dependencies": {
"better-sqlite3": "^12.11.1"
"better-sqlite3": "^12.10.0"
},
"engines": {
"node": ">=24.18.0",
+5 -5
View File
@@ -9,16 +9,16 @@ importers:
.:
dependencies:
better-sqlite3:
specifier: ^12.11.1
version: 12.11.1
specifier: ^12.10.0
version: 12.10.0
packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
better-sqlite3@12.11.1:
resolution: {integrity: sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==}
better-sqlite3@12.10.0:
resolution: {integrity: sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==}
engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x}
bindings@1.5.0:
@@ -148,7 +148,7 @@ snapshots:
base64-js@1.5.1: {}
better-sqlite3@12.11.1:
better-sqlite3@12.10.0:
dependencies:
bindings: 1.5.0
prebuild-install: 7.1.3
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "homarr",
"version": "1.69.2",
"version": "1.69.1",
"private": true,
"scripts": {
"build": "cross-env CI=true turbo build",
@@ -43,7 +43,6 @@
"@semantic-release/release-notes-generator": "catalog:",
"@testcontainers/redis": "catalog:",
"@turbo/gen": "catalog:",
"@vitejs/plugin-react": "catalog:",
"@vitest/coverage-v8": "catalog:",
"@vitest/ui": "catalog:",
+1 -1
View File
@@ -6,5 +6,5 @@ export function parseCookies(cookieString: string) {
}
export function setClientCookie(name: string, value: string, options: SerializeOptions = {}) {
document.cookie = stringifySetCookie({ name, value, ...options });
document.cookie = stringifySetCookie(name, value, options);
}
@@ -25,7 +25,7 @@ export class AxiosHttpErrorHandler extends HttpErrorHandler {
return new RequestError(requestErrorInput, {
cause: error,
}) as AnyRequestError;
});
}
handleResponseError(error: unknown): ResponseError | undefined {
if (!(error instanceof AxiosError)) return undefined;
@@ -20,7 +20,7 @@ export class FetchHttpErrorHandler extends HttpErrorHandler {
const result = matchErrorCode(error.cause.code);
if (!result) return undefined;
return new RequestError(result, { cause: error }) as AnyRequestError;
return new RequestError(result, { cause: error });
}
/**
@@ -59,7 +59,7 @@ export const matchErrorCode = (code: string): AnyRequestErrorInput | undefined =
type: key,
reason: found[0] as RequestErrorReason<typeof key>,
code: code as RequestErrorCode,
} as AnyRequestErrorInput;
};
}
return undefined;
@@ -30,7 +30,7 @@ export class NodeFetchHttpErrorHandler extends HttpErrorHandler {
return new RequestError(requestErrorInput, {
cause: error,
}) as AnyRequestError;
});
}
/**
@@ -60,8 +60,8 @@ export type HomarrDocumentationPath =
| "/docs/tags/connections"
| "/docs/tags/custom-widgets"
| "/docs/tags/customization"
| "/docs/tags/data-sources"
| "/docs/tags/data"
| "/docs/tags/data-sources"
| "/docs/tags/database"
| "/docs/tags/developer"
| "/docs/tags/development"
@@ -99,8 +99,8 @@ export type HomarrDocumentationPath =
| "/docs/tags/redis"
| "/docs/tags/responsive"
| "/docs/tags/roles"
| "/docs/tags/search-engines"
| "/docs/tags/search"
| "/docs/tags/search-engines"
| "/docs/tags/secrets"
| "/docs/tags/security"
| "/docs/tags/self-signed"
@@ -132,15 +132,6 @@ export type HomarrDocumentationPath =
| "/docs/advanced/running-as-different-user"
| "/docs/advanced/single-sign-on"
| "/docs/advanced/styling"
| "/docs/category/advanced"
| "/docs/category/community"
| "/docs/category/developer-guides"
| "/docs/category/getting-started"
| "/docs/category/installation-1"
| "/docs/category/installation"
| "/docs/category/integrations"
| "/docs/category/management"
| "/docs/category/widgets"
| "/docs/community/donate"
| "/docs/community/faq"
| "/docs/community/get-in-touch"
@@ -168,6 +159,7 @@ export type HomarrDocumentationPath =
| "/docs/integrations/archiveteam-warrior"
| "/docs/integrations/aria2"
| "/docs/integrations/audiobookshelf"
| "/docs/integrations/bazarr"
| "/docs/integrations/beszel"
| "/docs/integrations/coolify"
| "/docs/integrations/dash-dot"
@@ -192,6 +184,7 @@ export type HomarrDocumentationPath =
| "/docs/integrations/opnsense"
| "/docs/integrations/overseerr"
| "/docs/integrations/paperless-ngx"
| "/docs/integrations/patchmon"
| "/docs/integrations/peanut"
| "/docs/integrations/pi-hole"
| "/docs/integrations/plex"
@@ -205,9 +198,11 @@ export type HomarrDocumentationPath =
| "/docs/integrations/slskd"
| "/docs/integrations/sonarr"
| "/docs/integrations/speedtest-tracker"
| "/docs/integrations/synology"
| "/docs/integrations/tdarr"
| "/docs/integrations/technitium-dns"
| "/docs/integrations/tracearr"
| "/docs/integrations/traefik"
| "/docs/integrations/transmission"
| "/docs/integrations/truenas"
| "/docs/integrations/umami"
@@ -230,6 +225,7 @@ export type HomarrDocumentationPath =
| "/docs/widgets/anchor-note"
| "/docs/widgets/app"
| "/docs/widgets/audio-stats"
| "/docs/widgets/bazarr"
| "/docs/widgets/beszel-alerts"
| "/docs/widgets/beszel-system-grid"
| "/docs/widgets/beszel-system-stats"
@@ -249,6 +245,7 @@ export type HomarrDocumentationPath =
| "/docs/widgets/immich-album-carousel"
| "/docs/widgets/immich-server-stats"
| "/docs/widgets/indexer-manager"
| "/docs/widgets/media-missing"
| "/docs/widgets/media-releases"
| "/docs/widgets/media-request-list"
| "/docs/widgets/media-request-stats"
@@ -260,15 +257,18 @@ export type HomarrDocumentationPath =
| "/docs/widgets/notebook"
| "/docs/widgets/notifications"
| "/docs/widgets/paperless-ngx"
| "/docs/widgets/patchmon"
| "/docs/widgets/releases"
| "/docs/widgets/rss-feed"
| "/docs/widgets/smart-home-entity-state"
| "/docs/widgets/smart-home-execute-automation"
| "/docs/widgets/speedtest-tracker"
| "/docs/widgets/stock-price"
| "/docs/widgets/system-disks"
| "/docs/widgets/system-resources"
| "/docs/widgets/timetable"
| "/docs/widgets/tracearr"
| "/docs/widgets/traefik"
| "/docs/widgets/umami"
| "/docs/widgets/ups"
| "/docs/widgets/uptime-kuma"
@@ -72,14 +72,7 @@ export const AppForm = ({
}
form.initialize(toFormValues(initialValues));
// `form` is a new object every render (Mantine useForm returns a fresh literal),
// so it must not be a dep here — it would re-run every render and, since
// form.initialize() always calls clearErrors() (new {} object) internally,
// trigger an infinite update loop. initialValuesKey (a primitive string)
// already captures the only case this effect exists for: re-initializing
// when initialValues actually changes (e.g. navigating between edit pages).
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialValuesKey]);
}, [initialValuesKey, form, initialValues]);
// Debounce the name value with 200ms delay
const [debouncedName] = useDebouncedValue(form.values.name, 200);
@@ -0,0 +1,141 @@
import { describe, expect, test, vi } from "vitest";
vi.hoisted(() => {
process.env.SKIP_ENV_VALIDATION = "true";
process.env.SECRET_ENCRYPTION_KEY = "ff3f4f7ce30e870c9630de9e5d244ffa81101a24ed0dfe5f064beb53a7e684f1";
process.env.ENABLE_DNS_CACHING = "false";
});
const controllerCtor = vi.fn();
const controllerLogin = vi.fn().mockResolvedValue(true);
vi.mock("@homarr/redis", () => ({
createGetSetChannel: () => ({
getAsync: vi.fn().mockResolvedValue(null),
setAsync: vi.fn().mockResolvedValue(undefined),
removeAsync: vi.fn().mockResolvedValue(undefined),
}),
}));
vi.mock("@homarr/core/infrastructure/logs", () => ({
createLogger: () => ({ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }),
ErrorWithMetadata: class extends Error {},
}));
vi.mock("@homarr/core/infrastructure/http", () => ({
createCustomCheckServerIdentity: () => (() => undefined) as never,
fetchWithTrustedCertificatesAsync: vi.fn(),
createAxiosCertificateInstanceAsync: vi.fn().mockResolvedValue({}),
createCertificateAgentAsync: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("@homarr/core/infrastructure/certificates", () => ({
getTrustedCertificateHostnamesAsync: vi.fn().mockResolvedValue([]),
getAllTrustedCertificatesAsync: vi.fn().mockResolvedValue([]),
}));
vi.mock("axios", () => ({
AxiosError: Error,
default: {
create: vi.fn().mockReturnValue({}),
},
}));
vi.mock("http-cookie-agent/http", () => ({
HttpCookieAgent: class {},
HttpsCookieAgent: class {},
}));
vi.mock("@homarr/node-unifi", () => {
class FakeController {
public options: Record<string, unknown>;
constructor(options: Record<string, unknown>) {
this.options = options;
controllerCtor(options);
}
public async login(): Promise<true> {
return await controllerLogin(this.options.port);
}
public async getSitesStats(): Promise<unknown[]> {
return [];
}
}
return { default: { Controller: FakeController } };
});
import { UnifiControllerIntegration } from "../unifi-controller-integration";
const createIntegration = (url: string) =>
new UnifiControllerIntegration({
id: "test-unifi",
name: "Test Unifi",
url,
externalUrl: null,
decryptedSecrets: [
{ kind: "username", value: "admin" },
{ kind: "password", value: "secret" },
],
});
describe("UnifiControllerIntegration port resolution", () => {
test("a URL without a port uses the UniFi OS port 443", async () => {
controllerCtor.mockClear();
controllerLogin.mockClear().mockResolvedValue(true);
await createIntegration("http://192.168.1.1").getNetworkSummaryAsync();
expect(controllerCtor).toHaveBeenCalledTimes(1);
expect(controllerCtor).toHaveBeenCalledWith(expect.objectContaining({ host: "192.168.1.1", port: 443 }));
});
test("a connection failure on port 443 falls back to the self-hosted controller port 8443", async () => {
controllerCtor.mockClear();
controllerLogin
.mockClear()
.mockRejectedValueOnce(Object.assign(new Error("connect ECONNREFUSED"), { isAxiosError: true }))
.mockResolvedValueOnce(true);
await createIntegration("http://controller.lan").getNetworkSummaryAsync();
expect(controllerCtor).toHaveBeenNthCalledWith(1, expect.objectContaining({ port: 443 }));
expect(controllerCtor).toHaveBeenNthCalledWith(2, expect.objectContaining({ port: 8443 }));
});
test("https://controller.lan:8443 keeps the user-specified port", async () => {
controllerCtor.mockClear();
controllerLogin.mockClear().mockResolvedValue(true);
await createIntegration("https://controller.lan:8443").getNetworkSummaryAsync();
expect(controllerCtor).toHaveBeenCalledWith(expect.objectContaining({ host: "controller.lan", port: 8443 }));
});
test("https://192.168.1.1:8443 keeps the user-specified port", async () => {
controllerCtor.mockClear();
controllerLogin.mockClear().mockResolvedValue(true);
await createIntegration("https://192.168.1.1:8443").getNetworkSummaryAsync();
expect(controllerCtor).toHaveBeenCalledWith(expect.objectContaining({ host: "192.168.1.1", port: 8443 }));
});
test("an unusual port like 10443 is honored verbatim", async () => {
controllerCtor.mockClear();
controllerLogin.mockClear().mockResolvedValue(true);
await createIntegration("https://192.168.1.1:10443").getNetworkSummaryAsync();
expect(controllerCtor).toHaveBeenCalledWith(expect.objectContaining({ host: "192.168.1.1", port: 10443 }));
});
test("an authentication failure on port 443 does not try another port", async () => {
controllerCtor.mockClear();
controllerLogin.mockClear().mockRejectedValueOnce(
Object.assign(new Error("Request failed with status code 401"), {
isAxiosError: true,
response: { status: 401 },
}),
);
await expect(createIntegration("https://controller.lan").getNetworkSummaryAsync()).rejects.toThrow();
expect(controllerCtor).toHaveBeenCalledTimes(1);
expect(controllerCtor).toHaveBeenCalledWith(expect.objectContaining({ port: 443 }));
});
});
@@ -2,7 +2,6 @@ import type tls from "node:tls";
import axios from "axios";
import { HttpCookieAgent, HttpsCookieAgent } from "http-cookie-agent/http";
import { getPortFromUrl } from "@homarr/common";
import {
getAllTrustedCertificatesAsync,
getTrustedCertificateHostnamesAsync,
@@ -67,26 +66,53 @@ export class UnifiControllerIntegration extends Integration implements NetworkCo
checkServerIdentity: createCustomCheckServerIdentity(await getTrustedCertificateHostnamesAsync()),
};
const client = new Unifi.Controller({
host: url.hostname,
port: getPortFromUrl(url),
username: this.getSecretValue("username"),
password: this.getSecretValue("password"),
createAxiosInstance({ cookies }) {
return axios.create({
adapter: "http",
httpAgent: new HttpCookieAgent({ cookies }),
httpsAgent: new HttpsCookieAgent({
cookies,
requestCert: true,
...certificateOptions,
}),
});
},
});
// ponytail: node-unifi always connects over HTTPS, regardless of the protocol in the
// integration URL. UniFi OS consoles use port 443 while self-hosted controllers normally
// use 8443. If no port was provided, try both without hiding authentication errors.
const ports = url.port ? [Number(url.port)] : [443, 8443];
const createClientForPortAsync = async (port: number) => {
const client = new Unifi.Controller({
host: url.hostname,
port,
username: this.getSecretValue("username"),
password: this.getSecretValue("password"),
createAxiosInstance({ cookies }) {
return axios.create({
adapter: "http",
httpAgent: new HttpCookieAgent({ cookies }),
httpsAgent: new HttpsCookieAgent({
cookies,
requestCert: true,
...certificateOptions,
}),
});
},
});
await client.login(this.getSecretValue("username"), this.getSecretValue("password"), null);
return client;
await client.login(this.getSecretValue("username"), this.getSecretValue("password"), null);
return client;
};
for (const [index, port] of ports.entries()) {
try {
return await createClientForPortAsync(port);
} catch (error) {
const isLastPort = index === ports.length - 1;
if (isLastPort || !this.isPortFallbackError(error)) {
throw error;
}
}
}
throw new Error("Unable to connect to the UniFi controller");
}
private isPortFallbackError(error: unknown) {
if (typeof error === "object" && error !== null && "isAxiosError" in error && error.isAxiosError === true) {
return !("response" in error) || error.response === undefined;
}
return error instanceof Error && error.message === "failed to detect UniFiOS status";
}
private getStatusValueOverAllSites<S extends HealthSubsystem>(
+33 -33
View File
@@ -6,9 +6,9 @@
"subtitle": "",
"description": "",
"action": {
"scratch": "Sākt no jauna",
"scratch": "",
"importOldmarr": "",
"restoreSqlite": "SQLite rezerves kopija",
"restoreSqlite": "",
"restoreSqliteDescription": "",
"backToOptions": ""
}
@@ -26,19 +26,19 @@
}
},
"importSettings": {
"title": "Importēt iestatījumus",
"title": "",
"description": ""
},
"boardSelection": {
"title": "",
"description": "",
"action": {
"selectAll": "Atzīmēt visu",
"selectAll": "",
"unselectAll": ""
}
},
"summary": {
"title": "Importēšanas kopsavilkums",
"title": "",
"description": "",
"action": {
"import": ""
@@ -46,7 +46,7 @@
"entities": {
"apps": "Lietotnes",
"boards": "Dēļi",
"integrations": "Integrācijas",
"integrations": "",
"credentialUsers": ""
}
},
@@ -67,15 +67,15 @@
}
},
"user": {
"title": "Administratora lietotājs",
"title": "",
"subtitle": "",
"notification": {
"success": {
"title": "Lietotājs izveidots",
"title": "",
"message": ""
},
"error": {
"title": "Lietotāja izveide neizdevās"
"title": ""
}
}
},
@@ -94,19 +94,19 @@
"subtitle": ""
},
"integrations": {
"title": "Integrācijas",
"title": "",
"subtitle": "",
"description": "",
"action": {
"continue": "Turpināt",
"skip": "Pagaidām izlaist šo soli"
"continue": "",
"skip": ""
},
"configure": {
"progress": "{current} no {total}",
"skip": "Izlaist šo integrāciju",
"progress": "",
"skip": "",
"finishing": "",
"error": {
"title": "Neizdevās iestatīt!",
"title": "",
"message": ""
},
"helpers": {
@@ -115,7 +115,7 @@
}
},
"urlTemplate": {
"label": "Bāzes URL",
"label": "",
"placeholder": "",
"hint": "",
"warning": "",
@@ -136,14 +136,14 @@
}
},
"finish": {
"title": "Pabeigt Iestatīšanu",
"title": "",
"subtitle": "",
"description": "",
"action": {
"goToBoard": "",
"createBoard": "",
"inviteUser": "Ielūgt citus lietotājus",
"docs": "Lasīt dokumentāciju"
"inviteUser": "",
"docs": ""
}
}
},
@@ -158,7 +158,7 @@
"subtitle": ""
},
"invite": {
"title": "Pievienoties Homarr",
"title": "",
"subtitle": "",
"description": ""
},
@@ -177,20 +177,20 @@
},
"password": {
"label": "Parole",
"suggestionsTitle": "Paroles ieteikumi",
"suggestionsTitle": "",
"suggestion": {
"length": "Vismaz 8 rakstzīmes",
"length": "",
"lowercase": "Ietver mazo burtu",
"uppercase": "Ietver lielo burtu",
"number": "Ietver numuru",
"special": "Iekļauj speciālo simbolu"
"special": ""
}
},
"passwordConfirm": {
"label": "Apstipriniet paroli"
},
"previousPassword": {
"label": "Iepriekšējā parole"
"label": ""
},
"homeBoard": {
"label": ""
@@ -218,10 +218,10 @@
"notification": {
"success": {
"title": "",
"message": "Jūs esat veiksmīgi pieteicies"
"message": ""
},
"error": {
"title": "Pieteikšanās neizdevās",
"title": "",
"message": ""
}
},
@@ -245,13 +245,13 @@
},
"create": "Izveidot lietotāju",
"changePassword": {
"label": "Mainīt paroli",
"label": "",
"notification": {
"success": {
"message": "Parole veiksmīgi nomainīta."
"message": ""
},
"error": {
"message": "Nav iespējams nomainīt paroli"
"message": ""
}
}
},
@@ -595,12 +595,12 @@
}
},
"defaultGroup": {
"name": "Noklusētā grupa",
"name": "",
"description": ""
}
},
"app": {
"search": "Atrast aplikāciju",
"search": "",
"page": {
"list": {
"title": "Lietotnes",
@@ -689,7 +689,7 @@
"list": {
"title": "",
"noResults": "",
"loading": "Notiek ielāde..."
"loading": ""
},
"create": {
"title": "",
@@ -700,7 +700,7 @@
}
},
"action": {
"create": "Izveidot",
"create": "",
"import": "",
"save": "",
"delete": "",
+12 -12
View File
@@ -1599,7 +1599,7 @@
"menu": {
"label": {
"settings": "Ustawienia",
"refresh": "Odśwież",
"refresh": "",
"options": "Opcje",
"actions": "Akcje"
}
@@ -1904,7 +1904,7 @@
"noStation": "Nie wybrano stacji",
"option": {
"baseUrl": {
"label": "Szukaj adresu URL"
"label": ""
},
"station": {
"label": "Stacja"
@@ -2791,8 +2791,8 @@
"description": "Wyświetl datę zdjęcia oraz bieżący indeks na dole pokazu slajdów"
},
"randomizePhotos": {
"label": "Losowe zdjęcia",
"description": "Pokaż zdjęcia w losowej kolejności, zamiast zawsze rozpoczynać się od tej samej kolejności albumu."
"label": "",
"description": ""
}
},
"description": "Pokaz slajdów zdjęć z Twojego albumu w Immich",
@@ -3158,7 +3158,7 @@
"label": "Dostawca"
},
"providerUrl": {
"label": "URL dostawcy"
"label": ""
},
"identifier": {
"label": "Identyfikator",
@@ -3209,11 +3209,11 @@
"tooltip": "Nie udało się rozpoznać dostawcy. Ustaw go ręcznie po zaimportowaniu obrazów."
},
"tokens": {
"label": "Tokeny dostawcy",
"token": "Token",
"configured": "Token skonfigurowany",
"notConfigured": "Brak ustawionych tokenów",
"save": "Zapisz"
"label": "",
"token": "",
"configured": "",
"notConfigured": "",
"save": ""
}
}
},
@@ -3236,9 +3236,9 @@
"invalidIdentifier": "Nieprawidłowy identyfikator",
"noMatchingVersion": "Nie znaleziono pasującej wersji",
"noReleasesFound": "Nie znaleziono wydań!",
"noProviderSelected": "Nie wybrano dostawcy",
"noProviderSelected": "",
"noProviderResponse": "Brak odpowiedzi od dostawcy",
"unexpected": "Nieoczekiwany błąd dostawcy"
"unexpected": ""
}
}
},
+1 -1
View File
@@ -3437,7 +3437,7 @@
"description": ""
},
"maxNowPlayingItems": {
"label": "Максимум елементів для відтворення",
"label": "",
"description": ""
},
"showLibraryCount": {
+658 -704
View File
File diff suppressed because it is too large. Load diff
+31 -35
View File
@@ -14,7 +14,7 @@ catalog:
"@dnd-kit/utilities": ^3.2.2
"@drizzle-team/brocli": ^0.12.0
"@extractus/feed-extractor": 7.2.1
"@gfazioli/mantine-onboarding-tour": ^4.0.6
'@gfazioli/mantine-onboarding-tour': ^4.0.6
"@gitbeaker/rest": ^43.8.0
"@homarr/gridstack": ^1.12.0
"@homarr/node-unifi": ^2.6.0
@@ -40,16 +40,16 @@ catalog:
"@semantic-release/changelog": ^6.0.3
"@semantic-release/commit-analyzer": ^13.0.1
"@semantic-release/git": ^10.0.1
"@semantic-release/github": ^12.0.9
"@semantic-release/github": ^12.0.8
"@semantic-release/npm": ^13.1.5
"@semantic-release/release-notes-generator": ^14.1.1
"@t3-oss/env-nextjs": ^0.13.11
"@tabler/icons-react": ^3.44.0
"@tanstack/query-async-storage-persister": ^5.101.2
"@tanstack/react-query": ^5.101.2
"@tanstack/react-query-devtools": ^5.101.2
"@tanstack/react-query-next-experimental": ^5.101.2
"@tanstack/react-query-persist-client": ^5.101.2
"@tanstack/query-async-storage-persister": ^5.101.0
"@tanstack/react-query": ^5.101.0
"@tanstack/react-query-devtools": ^5.101.0
"@tanstack/react-query-next-experimental": ^5.101.0
"@tanstack/react-query-persist-client": ^5.101.0
"@testcontainers/mysql": ^12.0.4
"@testcontainers/postgresql": ^12.0.4
"@testcontainers/redis": ^12.0.4
@@ -69,12 +69,12 @@ catalog:
"@tiptap/extension-underline": 3.27.1
"@tiptap/react": ^3.27.1
"@tiptap/starter-kit": ^3.27.1
"@trpc/client": ^11.18.0
"@trpc/next": ^11.18.0
"@trpc/react-query": ^11.18.0
"@trpc/server": ^11.18.0
"@trpc/tanstack-react-query": ^11.18.0
"@turbo/gen": ^2.10.2
"@trpc/client": ^11.17.0
"@trpc/next": ^11.17.0
"@trpc/react-query": ^11.17.0
"@trpc/server": ^11.17.0
"@trpc/tanstack-react-query": ^11.17.0
"@turbo/gen": ^2.10.0
"@types/adm-zip": 0.5.8
"@types/bcrypt": 6.0.0
"@types/better-sqlite3": 7.6.13
@@ -89,29 +89,29 @@ catalog:
"@types/prismjs": ^1.26.6
"@types/react": 19.2.17
"@types/react-dom": 19.2.3
"@scalar/api-reference-react": ^0.9.50
"@scalar/api-reference-react": ^0.9.49
"@types/video.js": ^7.3.58
"@types/ws": ^8.18.1
"@types/xml2js": ^0.4.14
posthog-js: ^1.396.4
posthog-node: ^5.39.2
posthog-js: ^1.396.2
posthog-node: ^4.18.0
"@vitejs/plugin-react": ^6.0.3
"@vitest/coverage-v8": ^4.1.9
"@vitest/ui": ^4.1.9
"@xterm/addon-canvas": ^0.7.0
"@xterm/addon-fit": 0.11.0
"@xterm/xterm": ^6.0.0
adm-zip: 0.5.18
adm-zip: 0.5.17
jszip: ^3.10.1
sql.js: ^1.14.1
babel-plugin-react-compiler: ^1.0.0
bcrypt: ^6.0.0
better-sqlite3: ^12.11.1
better-sqlite3: ^12.10.0
chroma-js: ^3.2.0
clsx: ^2.1.1
concurrently: ^10.0.3
conventional-changelog-conventionalcommits: ^10.2.0
cookie: 2.0.1
conventional-changelog-conventionalcommits: ^9.3.1
cookie: 1.1.1
cookies: ^0.9.1
croner: 10.0.1
cross-env: ^10.1.0
@@ -139,7 +139,7 @@ catalog:
jsonpath-plus: ^10.4.0
ldapts: 8.1.8
lodash.clonedeep: ^4.5.0
lucide-react: ^1.23.0
lucide-react: ^1.22.0
"mantine-contextmenu": ^9.3.1
mantine-datatable: ^9.3.1
mantine-react-table: npm:mantine-react-table-open@9.0.3
@@ -148,11 +148,11 @@ catalog:
mysql2: 3.22.5
next: 16.2.9
next-auth: 5.0.0-beta.31
next-intl: 4.13.1
next-intl: 4.13.0
node-loader: ^2.1.0
octokit: ^5.0.5
oxfmt: ^0.57.0
oxlint: ^1.72.0
oxfmt: ^0.56.0
oxlint: ^1.71.0
pg: ^8.22.0
postcss-preset-mantine: ^1.18.0
prismjs: ^1.30.0
@@ -164,7 +164,7 @@ catalog:
react-markdown: ^10.1.0
react-quill-new: ^3.8.3
react-simple-code-editor: ^0.14.1
recharts: ^3.9.1
recharts: ^3.9.0
sass: ^1.101.0
semantic-release: ^25.0.5
semver-parser: ^4.2.1
@@ -174,11 +174,11 @@ catalog:
timezones-ical-library: ^2.2.1
trpc-to-mcp: ^1.3.2
trpc-to-openapi: ^3.3.0
tsdav: ^2.3.0
tsdav: ^2.2.2
tsx: 4.20.4
turbo: ^2.10.2
turbo: ^2.10.0
typescript: ^6.0.3
vite: ^8.1.2
vite: ^8.1.0
undici: 7.28.0
use-deep-compare-effect: ^1.8.1
video.js: ^8.23.9
@@ -191,7 +191,6 @@ catalog:
zod: ^4.4.3
zod-form-data: ^3.0.2
zod-validation-error: ^5.0.0
catalogMode: strict
nodeLinker: hoisted
strictPeerDependencies: false
@@ -208,32 +207,29 @@ minimumReleaseAgeExclude:
- form-data@4.0.6
# Renovate security update: undici@7.28.0
- undici@7.28.0
# Renovate security update: esbuild@0.28.1
- esbuild@0.28.1
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.18.1"
"brace-expansion@>=2.0.0 <=2.0.1": ">=5.0.7"
"brace-expansion@>=1.0.0 <=1.1.11": ">=5.0.7"
"esbuild@<=0.24.2": ">=0.28.1"
"esbuild@<=0.24.2": ">=0.28.0"
"form-data@>=4.0.0 <4.0.4": ">=4.0.6"
"hono@<4.6.5": ">=4.12.27"
"linkifyjs@<4.3.2": ">=4.3.3"
"nanoid@>=4.0.0 <5.0.9": ">=5.1.16"
"prismjs@<1.30.0": ">=1.30.0"
"proxmox-api>undici": "7.28.0"
"proxmox-api>undici": "7.27.1"
"react-is": "^19.2.7"
"rollup@>=4.0.0 <4.22.4": ">=4.62.2"
"sha.js@<=2.4.11": ">=2.4.12"
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.3"
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.3"
"tmp@<=0.2.3": ">=0.2.7"
"vite@>=5.0.0 <=5.4.18": ">=8.1.2"
"vite@>=5.0.0 <=5.4.18": ">=8.1.0"
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