Compare commits

..
41 changed files with 606 additions and 15690 deletions

No files matched your search

-6
View File
@@ -1,9 +1,3 @@
## [1.69.1](https://github.com/homarr-labs/homarr/compare/v1.69.0...v1.69.1) (2026-07-04)
### Bug Fixes
* unblock query cache mutations in demo mode and add cache buster ([#6176](https://github.com/homarr-labs/homarr/issues/6176)) ([a382c75](https://github.com/homarr-labs/homarr/commit/a382c75968e73bafff55b5890b1e95f0779b50d3))
## [1.69.0](https://github.com/homarr-labs/homarr/compare/v1.68.1...v1.69.0) (2026-07-03)
### Features
+8 -21
View File
@@ -39,26 +39,13 @@ import { immichServerStatsWidget } from '../../widgets/immich-server-stats';
'Scroll down to "API Keys"',
'Click "New API Key"',
'Enter a name, e.g. "Homarr"',
(
<span>
Select the following permissions:{" "}
<code>album.statistics</code>,{" "}
<code>album.read</code>,{" "}
<code>album.create</code>,{" "}
<code>user.read</code>,{" "}
<code>user.update</code>,{" "}
<code>asset.read</code>,{" "}
<code>asset.download</code>,{" "}
<code>asset.statistics</code>,{" "}
<code>server.statistics</code>,{" "}
<code>server.about</code>.
<br />
Additional permissions may be required depending on your Immich version. If you encounter authorization errors, grant any missing permissions requested by the server.
</span>
),Ï
(<span>
Select the following permissions:{" "}
<code>album.read</code>,{" "}
<code>user.read</code>,{" "}
<code>server.statistics</code>,{" "}
<code>asset.download</code>
</span>),
'Create the integration in Homarr and copy the API key to there'
]
}]} />
}]} />
@@ -1,48 +1,40 @@
---
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: []
}]} />
+13 -1
View File
@@ -70,4 +70,16 @@ API key authentication enables programmatic access for:
- Automation scripts and CI/CD pipelines
- Third-party integrations
- Custom dashboards and monitoring tools
- Container orchestration systems that need to sync app definitions
- Container orchestration systems that need to sync app definitions
## Board Automation
The OpenAPI and MCP APIs include board management endpoints for common automation workflows:
- Create, duplicate, rename and delete boards
- Change board visibility
- Update board visual and behavior settings
- Set desktop and mobile home boards for the current user
- Configure global board defaults such as public home boards and status behavior
Use the interactive API reference to inspect the exact request body for each endpoint under the `boards` and `settings` tags.
+2 -2
View File
@@ -32,7 +32,7 @@
"@scalar/api-reference-react": "catalog:",
"@signalwire/docusaurus-plugin-llms-txt": "^1.2.2",
"@tabler/icons-react": "catalog:",
"@tailwindcss/postcss": "^4.3.2",
"@tailwindcss/postcss": "^4.3.1",
"clsx": "catalog:",
"dayjs": "catalog:",
"docusaurus-plugin-image-zoom": "^3.0.1",
@@ -44,7 +44,7 @@
"react": "catalog:",
"react-dom": "catalog:",
"swiper": "^12.2.0",
"tailwindcss": "^4.3.2"
"tailwindcss": "^4.3.1"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.10.1",
+1 -1
View File
@@ -1 +1 @@
{"openapi":"3.1.0","info":{"title":"Homarr API documentation","version":"1.0.0"},"servers":[{"url":"http://localhost:3000"}],"externalDocs":{"url":"https://homarr.dev"},"paths":{"/api/apps/paginated":{"get":{"operationId":"appRouter-getPaginated","tags":["apps"],"security":[{"apikey":[]}],"parameters":[{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"pageSize","schema":{"default":10,"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},{"in":"query","name":"page","schema":{"default":1,"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"iconUrl":{"type":"string"},"href":{"anyOf":[{"type":"string"},{"type":"null"}]},"pingUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","description","iconUrl","href","pingUrl"],"additionalProperties":false}},"totalCount":{"type":"number"}},"required":["items","totalCount"],"additionalProperties":false}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/apps":{"get":{"operationId":"appRouter-all","tags":["apps"],"security":[{"apikey":[]}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"iconUrl":{"type":"string"},"href":{"anyOf":[{"type":"string"},{"type":"null"}]},"pingUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","description","iconUrl","href","pingUrl"],"additionalProperties":false}}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}},"post":{"operationId":"appRouter-create","tags":["apps"],"security":[{"apikey":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":64},"description":{"anyOf":[{"type":"string","maxLength":512},{"type":"null"}]},"iconUrl":{"type":"string","minLength":1},"href":{"anyOf":[{"anyOf":[{"type":"string","pattern":"^(?!javascript)[a-zA-Z]*:\\/\\/"},{"type":"string","const":""}]},{"type":"null"}]},"pingUrl":{"anyOf":[{"anyOf":[{"type":"string","pattern":"^https?:\\/\\/"},{"type":"string","const":""}]},{"type":"null"}]}},"required":["name","description","iconUrl","href","pingUrl"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"allOf":[{"type":"object","properties":{"appId":{"type":"string"}},"required":["appId"],"additionalProperties":false},{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"iconUrl":{"type":"string"},"href":{"anyOf":[{"type":"string"},{"type":"null"}]},"pingUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","description","iconUrl","href","pingUrl"],"additionalProperties":false}]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/apps/search":{"get":{"operationId":"appRouter-search","tags":["apps"],"security":[{"apikey":[]}],"parameters":[{"in":"query","name":"query","schema":{"type":"string"},"required":true},{"in":"queLine truncated
{"openapi":"3.1.0","info":{"title":"Homarr API documentation","version":"1.1.0"},"servers":[{"url":"http://localhost:3000"}],"externalDocs":{"url":"https://homarr.dev"},"paths":{"/api/apps/paginated":{"get":{"operationId":"appRouter-getPaginated","tags":["apps"],"security":[{"apikey":[]}],"parameters":[{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"pageSize","schema":{"default":10,"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},{"in":"query","name":"page","schema":{"default":1,"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"iconUrl":{"type":"string"},"href":{"anyOf":[{"type":"string"},{"type":"null"}]},"pingUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","description","iconUrl","href","pingUrl"],"additionalProperties":false}},"totalCount":{"type":"number"}},"required":["items","totalCount"],"additionalProperties":false}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/apps":{"get":{"operationId":"appRouter-all","tags":["apps"],"security":[{"apikey":[]}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"iconUrl":{"type":"string"},"href":{"anyOf":[{"type":"string"},{"type":"null"}]},"pingUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","description","iconUrl","href","pingUrl"],"additionalProperties":false}}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}},"post":{"operationId":"appRouter-create","tags":["apps"],"security":[{"apikey":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":64},"description":{"anyOf":[{"type":"string","maxLength":512},{"type":"null"}]},"iconUrl":{"type":"string","minLength":1},"href":{"anyOf":[{"anyOf":[{"type":"string","pattern":"^(?!javascript)[a-zA-Z]*:\\/\\/"},{"type":"string","const":""}]},{"type":"null"}]},"pingUrl":{"anyOf":[{"anyOf":[{"type":"string","pattern":"^https?:\\/\\/"},{"type":"string","const":""}]},{"type":"null"}]}},"required":["name","description","iconUrl","href","pingUrl"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"allOf":[{"type":"object","properties":{"appId":{"type":"string"}},"required":["appId"],"additionalProperties":false},{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"iconUrl":{"type":"string"},"href":{"anyOf":[{"type":"string"},{"type":"null"}]},"pingUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","description","iconUrl","href","pingUrl"],"additionalProperties":false}]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"description":"Authorization not provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.UNAUTHORIZED"}}}},"403":{"description":"Insufficient access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.FORBIDDEN"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/api/apps/search":{"get":{"operationId":"appRouter-search","tags":["apps"],"security":[{"apikey":[]}],"parameters":[{"in":"query","name":"query","schema":{"type":"string"},"required":true},{"in":"queLine truncated
@@ -24,7 +24,6 @@ import type { AppRouter } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import {
isPersistableWidgetQueryKey,
queryCacheBuster,
queryCacheDefaultGcTimeMs,
queryCacheDefaultRefetchIntervalMs,
queryCacheDefaultStaleTimeMs,
@@ -137,7 +136,6 @@ export function TRPCReactProvider(props: PropsWithChildren) {
client={queryClient}
persistOptions={{
persister,
buster: queryCacheBuster,
maxAge: queryCacheDefaultGcTimeMs,
dehydrateOptions: {
shouldDehydrateQuery: (query) =>
@@ -7,8 +7,6 @@ import "~/styles/gridstack.scss";
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
import type { PersistedClient } from "@tanstack/react-query-persist-client";
import superjson from "superjson";
import { queryCacheBuster } from "@homarr/api/query-cache";
import { getQueryClient } from "@homarr/api/server";
import { IntegrationProvider } from "@homarr/auth/client";
import { auth } from "@homarr/auth/next";
@@ -123,8 +121,7 @@ async function QueryCacheHydration({ userId, boardId }: { userId: string; boardI
const serialized = await getQueryCacheAsync(userId, boardId);
if (!serialized) return null;
const persisted = superjson.parse<PersistedClient>(serialized);
if (persisted.buster !== queryCacheBuster) return null;
const persisted = JSON.parse(serialized) as PersistedClient;
if (!persisted?.clientState?.queries?.length) return null;
return <HydrationBoundary state={persisted.clientState} />;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "homarr",
"version": "1.69.1",
"version": "1.69.0",
"private": true,
"scripts": {
"build": "cross-env CI=true turbo build",
+2
View File
@@ -9,6 +9,7 @@ import { iconsRouter } from "./router/icons";
import { infoRouter } from "./router/info";
import { integrationRouter } from "./router/integration/integration-router";
import { inviteRouter } from "./router/invite";
import { serverSettingsRouter } from "./router/serverSettings";
import { beszelRouter } from "./router/widgets/beszel";
import { calendarRouter } from "./router/widgets/calendar";
import { dnsHoleRouter } from "./router/widgets/dns-hole";
@@ -28,6 +29,7 @@ export const mcpRouter = createTRPCRouter({
info: infoRouter,
integration: integrationRouter,
invite: inviteRouter,
serverSettings: serverSettingsRouter,
beszel: beszelRouter,
calendar: calendarRouter,
dnsHole: dnsHoleRouter,
+3 -1
View File
@@ -6,6 +6,7 @@ import { appRouter } from "./router/app";
import { boardRouter } from "./router/board";
import { infoRouter } from "./router/info";
import { inviteRouter } from "./router/invite";
import { serverSettingsRouter } from "./router/serverSettings";
import { userRouter } from "./router/user";
import { createTRPCRouter } from "./trpc";
@@ -14,13 +15,14 @@ export const openApiRouter = createTRPCRouter({
boardRouter,
infoRouter,
inviteRouter,
serverSettingsRouter,
userRouter,
});
export const openApiDocument = (base: string) =>
generateOpenApiDocument(openApiRouter, {
title: "Homarr API documentation",
version: "1.0.0",
version: "1.1.0",
baseUrl: base,
docsUrl: "https://homarr.dev",
securitySchemes: {
-1
View File
@@ -5,7 +5,6 @@ export const queryCacheDefaultRefetchIntervalMs = 30_000;
export const queryCacheDefaultGcTimeMs = 1000 * 60 * 60 * 24;
export const queryCacheMaxValueBytes = 1024 * 1024;
export const queryCacheStoragePrefix = "homarr-widget-query";
export const queryCacheBuster = "v2-superjson";
let activeQueryCacheBoardId: string | null = null;
+95 -31
View File
@@ -330,7 +330,16 @@ export const boardRouter = createTRPCRouter({
}),
duplicateBoard: permissionRequiredProcedure
.requiresPermission("board-create")
.meta({
openapi: { method: "POST", path: "/api/boards/{id}/duplicate", tags: ["boards"], protect: true },
mcp: {
enabled: true,
description:
"Duplicate an existing board into a new board. Requires board-create permission and view permission on the source board. REQUIRED: id (source board ID), name (unique name for the new board). Returns { boardId }",
},
})
.input(boardDuplicateSchema)
.output(z.object({ boardId: z.string() }))
.mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "view");
await noBoardWithSimilarNameAsync(ctx.db, input.name);
@@ -544,33 +553,57 @@ export const boardRouter = createTRPCRouter({
});
},
});
return { boardId: newBoardId };
}),
renameBoard: protectedProcedure.input(boardRenameSchema).mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
renameBoard: protectedProcedure
.meta({
openapi: { method: "PATCH", path: "/api/boards/{id}/name", tags: ["boards"], protect: true },
mcp: {
enabled: true,
description:
"Rename a board by ID. Requires full permission on the board. REQUIRED: id (board ID), name (new unique board name)",
},
})
.input(boardRenameSchema)
.output(z.void())
.mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
await noBoardWithSimilarNameAsync(ctx.db, input.name, [input.id]);
await noBoardWithSimilarNameAsync(ctx.db, input.name, [input.id]);
await ctx.db.update(boards).set({ name: input.name }).where(eq(boards.id, input.id));
}),
changeBoardVisibility: protectedProcedure.input(boardChangeVisibilitySchema).mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
const boardSettings = await getServerSettingByKeyAsync(ctx.db, "board");
await ctx.db.update(boards).set({ name: input.name }).where(eq(boards.id, input.id));
}),
changeBoardVisibility: protectedProcedure
.meta({
openapi: { method: "PATCH", path: "/api/boards/{id}/visibility", tags: ["boards"], protect: true },
mcp: {
enabled: true,
description:
"Change board visibility. Requires full permission on the board. REQUIRED: id (board ID), visibility ('public' or 'private'). Home boards cannot be made private",
},
})
.input(boardChangeVisibilitySchema)
.output(z.void())
.mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
const boardSettings = await getServerSettingByKeyAsync(ctx.db, "board");
if (
input.visibility !== "public" &&
(boardSettings.homeBoardId === input.id || boardSettings.mobileHomeBoardId === input.id)
) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Cannot make home board private",
});
}
if (
input.visibility !== "public" &&
(boardSettings.homeBoardId === input.id || boardSettings.mobileHomeBoardId === input.id)
) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Cannot make home board private",
});
}
await ctx.db
.update(boards)
.set({ isPublic: input.visibility === "public" })
.where(eq(boards.id, input.id));
}),
await ctx.db
.update(boards)
.set({ isPublic: input.visibility === "public" })
.where(eq(boards.id, input.id));
}),
deleteBoard: protectedProcedure
.meta({
openapi: { method: "DELETE", path: "/api/boards/{id}", tags: ["boards"], protect: true },
@@ -587,16 +620,38 @@ export const boardRouter = createTRPCRouter({
await ctx.db.delete(boards).where(eq(boards.id, input.id));
}),
setHomeBoard: protectedProcedure.input(z.object({ id: z.string() })).mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "view");
setHomeBoard: protectedProcedure
.meta({
openapi: { method: "PATCH", path: "/api/boards/{id}/home", tags: ["boards"], protect: true },
mcp: {
enabled: true,
description:
"Set the current user's desktop home board. Requires view permission on the board. REQUIRED: id (board ID)",
},
})
.input(z.object({ id: z.string() }))
.output(z.void())
.mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "view");
await ctx.db.update(users).set({ homeBoardId: input.id }).where(eq(users.id, ctx.session.user.id));
}),
setMobileHomeBoard: protectedProcedure.input(z.object({ id: z.string() })).mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "view");
await ctx.db.update(users).set({ homeBoardId: input.id }).where(eq(users.id, ctx.session.user.id));
}),
setMobileHomeBoard: protectedProcedure
.meta({
openapi: { method: "PATCH", path: "/api/boards/{id}/mobile-home", tags: ["boards"], protect: true },
mcp: {
enabled: true,
description:
"Set the current user's mobile home board. Requires view permission on the board. REQUIRED: id (board ID)",
},
})
.input(z.object({ id: z.string() }))
.output(z.void())
.mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "view");
await ctx.db.update(users).set({ mobileHomeBoardId: input.id }).where(eq(users.id, ctx.session.user.id));
}),
await ctx.db.update(users).set({ mobileHomeBoardId: input.id }).where(eq(users.id, ctx.session.user.id));
}),
getHomeBoard: publicProcedure.query(async ({ ctx }) => {
const userId = ctx.session?.user.id;
const user = userId
@@ -752,7 +807,16 @@ export const boardRouter = createTRPCRouter({
}
}),
savePartialBoardSettings: protectedProcedure
.input(boardSavePartialSettingsSchema.and(z.object({ id: z.string() })))
.meta({
openapi: { method: "PATCH", path: "/api/boards/{id}/settings", tags: ["boards"], protect: true },
mcp: {
enabled: true,
description:
"Update visual and behavior settings for a board. Requires modify permission. REQUIRED: id (board ID). Optional fields include pageTitle, metaTitle, logoImageUrl, faviconImageUrl, backgroundImageUrl, colors, opacity, customCss, itemRadius, and disableStatus",
},
})
.input(boardSavePartialSettingsSchema.extend({ id: z.string() }))
.output(z.void())
.mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
+3 -3
View File
@@ -5,7 +5,7 @@ import { boards } from "@homarr/db/schema";
import { removeQueryCacheAsync, setQueryCacheAsync } from "@homarr/redis";
import { queryCacheDefaultGcTimeMs, queryCacheMaxValueBytes } from "../query-cache";
import { createTRPCRouter, internalProcedure } from "../trpc";
import { createTRPCRouter, publicProcedure } from "../trpc";
import { throwIfActionForbiddenAsync } from "./board/board-access";
const queryCacheBoardInput = z.object({
@@ -21,7 +21,7 @@ const validateBoardAccessAsync = async (ctx: Parameters<typeof throwIfActionForb
};
export const queryCacheRouter = createTRPCRouter({
setItem: internalProcedure.input(queryCacheSetInput).mutation(async ({ ctx, input }) => {
setItem: publicProcedure.input(queryCacheSetInput).mutation(async ({ ctx, input }) => {
await validateBoardAccessAsync(ctx, input.boardId);
const userId = ctx.session?.user.id ?? "anonymous";
return {
@@ -34,7 +34,7 @@ export const queryCacheRouter = createTRPCRouter({
),
};
}),
removeItem: internalProcedure.input(queryCacheBoardInput).mutation(async ({ ctx, input }) => {
removeItem: publicProcedure.input(queryCacheBoardInput).mutation(async ({ ctx, input }) => {
await validateBoardAccessAsync(ctx, input.boardId);
const userId = ctx.session?.user.id ?? "anonymous";
await removeQueryCacheAsync(userId, input.boardId);
+79 -1
View File
@@ -1,6 +1,14 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod/v4";
import { getServerSettingByKeyAsync, getServerSettingsAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries";
import { and, eq, inArray } from "@homarr/db";
import {
getServerSettingByKeyAsync,
getServerSettingsAsync,
insertServerSettingByKeyAsync,
updateServerSettingByKeyAsync,
} from "@homarr/db/queries";
import { boards, serverSettings } from "@homarr/db/schema";
import type { ServerSettings } from "@homarr/server-settings";
import { defaultServerSettingsKeys } from "@homarr/server-settings";
import { settingsInitSchema } from "@homarr/validation/settings";
@@ -8,6 +16,15 @@ import { settingsInitSchema } from "@homarr/validation/settings";
import { createTRPCRouter, onboardingProcedure, permissionRequiredProcedure, publicProcedure } from "../trpc";
import { nextOnboardingStepAsync } from "./onboard/onboard-queries";
const boardServerSettingsSchema = z.object({
homeBoardId: z.string().nullable(),
mobileHomeBoardId: z.string().nullable(),
enableStatusByDefault: z.boolean(),
forceDisableStatus: z.boolean(),
}) satisfies z.ZodType<ServerSettings["board"]>;
const boardServerSettingsUpdateSchema = boardServerSettingsSchema.partial();
export const serverSettingsRouter = createTRPCRouter({
getCulture: publicProcedure.query(async ({ ctx }) => {
return await getServerSettingByKeyAsync(ctx.db, "culture");
@@ -15,6 +32,67 @@ export const serverSettingsRouter = createTRPCRouter({
getAll: permissionRequiredProcedure.requiresPermission("admin").query(async ({ ctx }) => {
return await getServerSettingsAsync(ctx.db);
}),
getBoardSettings: permissionRequiredProcedure
.requiresPermission("admin")
.meta({
openapi: { method: "GET", path: "/api/settings/board", tags: ["settings"], protect: true },
mcp: {
enabled: true,
description:
"Get global board defaults, including desktop/mobile home board IDs and status behavior. Requires admin permission",
},
})
.input(z.void())
.output(boardServerSettingsSchema)
.query(async ({ ctx }) => {
return await getServerSettingByKeyAsync(ctx.db, "board");
}),
updateBoardSettings: permissionRequiredProcedure
.requiresPermission("admin")
.meta({
openapi: { method: "PATCH", path: "/api/settings/board", tags: ["settings"], protect: true },
mcp: {
enabled: true,
description:
"Update global board defaults. Requires admin permission. Optional fields: homeBoardId, mobileHomeBoardId, enableStatusByDefault, forceDisableStatus. Home board IDs must reference public boards or be null",
},
})
.input(boardServerSettingsUpdateSchema)
.output(boardServerSettingsSchema)
.mutation(async ({ ctx, input }) => {
const inputBoardIds = [input.homeBoardId, input.mobileHomeBoardId].filter(
(id) => id !== undefined && id !== null,
);
if (inputBoardIds.length > 0) {
const publicBoards = await ctx.db.query.boards.findMany({
columns: { id: true },
where: and(inArray(boards.id, inputBoardIds), eq(boards.isPublic, true)),
});
const publicBoardIds = new Set(publicBoards.map((board) => board.id));
const invalidBoardIds = inputBoardIds.filter((id) => !publicBoardIds.has(id));
if (invalidBoardIds.length > 0) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Board settings home board IDs must reference public boards: ${invalidBoardIds.join(", ")}`,
});
}
}
const current = await getServerSettingByKeyAsync(ctx.db, "board");
const next = { ...current, ...input };
const existing = await ctx.db.query.serverSettings.findFirst({
where: eq(serverSettings.settingKey, "board"),
});
if (existing) {
await updateServerSettingByKeyAsync(ctx.db, "board", next);
} else {
await insertServerSettingByKeyAsync(ctx.db, "board", next);
}
return next;
}),
saveSettings: permissionRequiredProcedure
.requiresPermission("admin")
.input(
@@ -1,9 +1,9 @@
import SuperJSON from "superjson";
import { stringify } from "superjson";
import { describe, expect, test, vi } from "vitest";
import type { Session } from "@homarr/auth";
import { createId } from "@homarr/common";
import { serverSettings } from "@homarr/db/schema";
import { boards, serverSettings } from "@homarr/db/schema";
import { createDb } from "@homarr/db/test";
import { defaultServerSettings, defaultServerSettingsKeys } from "@homarr/server-settings";
@@ -33,7 +33,7 @@ describe("getAll server settings", () => {
await db.insert(serverSettings).values([
{
settingKey: defaultServerSettingsKeys[0],
value: SuperJSON.stringify(defaultServerSettings.analytics),
value: stringify(defaultServerSettings.analytics),
},
]);
@@ -67,7 +67,7 @@ describe("saveSettings", () => {
await db.insert(serverSettings).values([
{
settingKey: defaultServerSettingsKeys[0],
value: SuperJSON.stringify(defaultServerSettings.analytics),
value: stringify(defaultServerSettings.analytics),
},
]);
@@ -82,7 +82,7 @@ describe("saveSettings", () => {
expect(dbSettings).toStrictEqual([
{
settingKey: "analytics",
value: SuperJSON.stringify({
value: stringify({
enableGeneral: true,
instanceId: null,
}),
@@ -90,3 +90,91 @@ describe("saveSettings", () => {
]);
});
});
describe("board settings API", () => {
test("getBoardSettings should return board defaults", async () => {
const db = createDb();
const caller = serverSettingsRouter.createCaller({
db,
deviceType: undefined,
session: defaultSession,
});
await expect(caller.getBoardSettings()).resolves.toStrictEqual(defaultServerSettings.board);
});
test("updateBoardSettings should insert settings when defaults are not persisted", async () => {
const db = createDb();
const caller = serverSettingsRouter.createCaller({
db,
deviceType: undefined,
session: defaultSession,
});
const boardId = createId();
await db.insert(boards).values({ id: boardId, name: "default", isPublic: true });
const result = await caller.updateBoardSettings({
homeBoardId: boardId,
forceDisableStatus: true,
});
expect(result).toStrictEqual({
...defaultServerSettings.board,
homeBoardId: boardId,
forceDisableStatus: true,
});
const dbSettings = await db.select().from(serverSettings);
expect(dbSettings).toStrictEqual([
{
settingKey: "board",
value: stringify(result),
},
]);
});
test("updateBoardSettings should update existing settings", async () => {
const db = createDb();
const caller = serverSettingsRouter.createCaller({
db,
deviceType: undefined,
session: defaultSession,
});
const boardId = createId();
await db.insert(boards).values({ id: boardId, name: "default", isPublic: true });
await caller.updateBoardSettings({ homeBoardId: boardId });
const result = await caller.updateBoardSettings({
mobileHomeBoardId: boardId,
enableStatusByDefault: false,
});
expect(result).toStrictEqual({
...defaultServerSettings.board,
homeBoardId: boardId,
mobileHomeBoardId: boardId,
enableStatusByDefault: false,
});
const dbSettings = await db.select().from(serverSettings);
expect(dbSettings).toStrictEqual([
{
settingKey: "board",
value: stringify(result),
},
]);
});
test("updateBoardSettings should reject private home boards", async () => {
const db = createDb();
const caller = serverSettingsRouter.createCaller({
db,
deviceType: undefined,
session: defaultSession,
});
const boardId = createId();
await db.insert(boards).values({ id: boardId, name: "private", isPublic: false });
const actAsync = async () => await caller.updateBoardSettings({ homeBoardId: boardId });
await expect(actAsync()).rejects.toThrow("must reference public boards");
});
});
+8
View File
@@ -2,16 +2,20 @@ import { expect, test, vi } from "vitest";
import { extractToolsFromProcedures } from "trpc-to-mcp";
import { appRouter } from "../router/app";
import { boardRouter } from "../router/board";
import { infoRouter } from "../router/info";
import { inviteRouter } from "../router/invite";
import { serverSettingsRouter } from "../router/serverSettings";
import { createTRPCRouter } from "../trpc";
vi.mock("@homarr/auth", () => ({}));
const mcpTestRouter = createTRPCRouter({
app: appRouter,
board: boardRouter,
info: infoRouter,
invite: inviteRouter,
serverSettings: serverSettingsRouter,
});
test("MCP tools should be extractable from the router", () => {
@@ -28,8 +32,12 @@ test("MCP tools should contain expected procedures", () => {
expect(toolNames).toContain("app_all");
expect(toolNames).toContain("app_byId");
expect(toolNames).toContain("app_create");
expect(toolNames).toContain("board_savePartialBoardSettings");
expect(toolNames).toContain("board_duplicateBoard");
expect(toolNames).toContain("info_getInfo");
expect(toolNames).toContain("invite_getAll");
expect(toolNames).toContain("serverSettings_getBoardSettings");
expect(toolNames).toContain("serverSettings_updateBoardSettings");
});
test("MCP tools should have descriptions", () => {
+9
View File
@@ -14,3 +14,12 @@ test("OpenAPI documentation should be generated", () => {
// Assert
expect(act).not.toThrow();
});
test("OpenAPI documentation should expose board automation endpoints", () => {
const document = openApiDocument("https://homarr.dev");
expect(document.info.version).toBe("1.1.0");
expect(document.paths).toHaveProperty("/api/boards/{id}/settings");
expect(document.paths).toHaveProperty("/api/boards/{id}/duplicate");
expect(document.paths).toHaveProperty("/api/settings/board");
});
-2
View File
@@ -116,8 +116,6 @@ const baseProcedure = env.DEMO_MODE ? t.procedure.use(enforceDemoModeReadOnly) :
*/
export const publicProcedure = baseProcedure;
export const internalProcedure = t.procedure;
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
@@ -1,9 +0,0 @@
CREATE TABLE `widget_secret` (
`item_id` varchar(64) NOT NULL,
`kind` varchar(64) NOT NULL,
`value` text NOT NULL,
`updated_at` timestamp NOT NULL,
CONSTRAINT `widget_secret_item_id_kind_pk` PRIMARY KEY(`item_id`,`kind`)
);
--> statement-breakpoint
ALTER TABLE `widget_secret` ADD CONSTRAINT `widget_secret_item_id_item_id_fk` FOREIGN KEY (`item_id`) REFERENCES `item`(`id`) ON DELETE cascade ON UPDATE no action;
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -295,13 +295,6 @@
"when": 1780764500000,
"tag": "0041_merge_custom_widget_url",
"breakpoints": true
},
{
"idx": 42,
"version": "5",
"when": 1783176100870,
"tag": "0042_add_widget_secret",
"breakpoints": true
}
]
}
@@ -1,9 +0,0 @@
CREATE TABLE "widget_secret" (
"item_id" varchar(64) NOT NULL,
"kind" varchar(64) NOT NULL,
"value" text NOT NULL,
"updated_at" timestamp NOT NULL,
CONSTRAINT "widget_secret_item_id_kind_pk" PRIMARY KEY("item_id","kind")
);
--> statement-breakpoint
ALTER TABLE "widget_secret" ADD CONSTRAINT "widget_secret_item_id_item_id_fk" FOREIGN KEY ("item_id") REFERENCES "public"."item"("id") ON DELETE cascade ON UPDATE no action;
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -57,13 +57,6 @@
"when": 1780764500000,
"tag": "0007_merge_custom_widget_url",
"breakpoints": true
},
{
"idx": 8,
"version": "7",
"when": 1783176100870,
"tag": "0008_add_widget_secret",
"breakpoints": true
}
]
}
@@ -1,8 +0,0 @@
CREATE TABLE `widget_secret` (
`item_id` text NOT NULL,
`kind` text NOT NULL,
`value` text NOT NULL,
`updated_at` integer NOT NULL,
PRIMARY KEY(`item_id`, `kind`),
FOREIGN KEY (`item_id`) REFERENCES `item`(`id`) ON UPDATE no action ON DELETE cascade
);
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -281,13 +281,6 @@
"when": 1780764500000,
"tag": "0039_merge_custom_widget_url",
"breakpoints": true
},
{
"idx": 40,
"version": "6",
"when": 1783176100870,
"tag": "0040_add_widget_secret",
"breakpoints": true
}
]
}
@@ -159,7 +159,6 @@ 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"
@@ -184,7 +183,6 @@ 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"
@@ -225,7 +223,6 @@ 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"
@@ -245,7 +242,6 @@ 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"
@@ -257,7 +253,6 @@ 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"
@@ -1,141 +0,0 @@
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,6 +2,7 @@ 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,
@@ -66,53 +67,26 @@ export class UnifiControllerIntegration extends Integration implements NetworkCo
checkServerIdentity: createCustomCheckServerIdentity(await getTrustedCertificateHostnamesAsync()),
};
// 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,
}),
});
},
});
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,
}),
});
},
});
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";
await client.login(this.getSecretValue("username"), this.getSecretValue("password"), null);
return client;
}
private getStatusValueOverAllSites<S extends HealthSubsystem>(
@@ -6,9 +6,12 @@ interface Options<TData, TInput extends Record<string, unknown>> {
type CacheEntry<TData> = { data: TData; timestamp: Date; expiresAt: number };
const MAX_CACHE_SIZE = 1000;
const cache = new Map<string, CacheEntry<unknown>>();
const inflight = new Map<string, Promise<CacheEntry<unknown>>>();
const DEFAULT_TTL_MS = 10_000;
const evictExpired = <TData>(cache: Map<string, CacheEntry<TData>>) => {
const evictExpired = () => {
const now = Date.now();
for (const [key, entry] of cache) {
if (now >= entry.expiresAt) cache.delete(key);
@@ -17,46 +20,41 @@ const evictExpired = <TData>(cache: Map<string, CacheEntry<TData>>) => {
export const createRequestHandler = <TData, TInput extends Record<string, unknown>>(
options: Options<TData, TInput>,
) => {
const cache = new Map<string, CacheEntry<TData>>();
const inflight = new Map<string, Promise<CacheEntry<TData>>>();
) => ({
handler: (input: TInput) => ({
async getDataAsync(): Promise<{ data: TData; timestamp: Date }> {
const ttl = options.cacheTtlMs ?? DEFAULT_TTL_MS;
const key = JSON.stringify(input);
return {
handler: (input: TInput) => ({
async getDataAsync(): Promise<{ data: TData; timestamp: Date }> {
const ttl = options.cacheTtlMs ?? DEFAULT_TTL_MS;
const key = JSON.stringify(input);
const cached = cache.get(key) as CacheEntry<TData> | undefined;
if (cached && Date.now() < cached.expiresAt) {
return { data: cached.data, timestamp: cached.timestamp };
}
if (cached) cache.delete(key);
const cached = cache.get(key);
if (cached && Date.now() < cached.expiresAt) {
return { data: cached.data, timestamp: cached.timestamp };
}
if (cached) cache.delete(key);
const existing = inflight.get(key);
if (existing) return existing as Promise<CacheEntry<TData>>;
const existing = inflight.get(key);
if (existing) return existing;
const promise = options
.requestAsync(input)
.then((data) => {
if (cache.size >= MAX_CACHE_SIZE) evictExpired();
if (cache.size >= MAX_CACHE_SIZE) {
const oldest = cache.keys().next().value;
if (oldest) cache.delete(oldest);
}
const entry: CacheEntry<TData> = { data, timestamp: new Date(), expiresAt: Date.now() + ttl };
cache.set(key, entry as CacheEntry<unknown>);
inflight.delete(key);
return entry;
})
.catch((err) => {
inflight.delete(key);
throw err;
});
const promise = options
.requestAsync(input)
.then((data) => {
if (cache.size >= MAX_CACHE_SIZE) evictExpired(cache);
if (cache.size >= MAX_CACHE_SIZE) {
const oldest = cache.keys().next().value;
if (oldest) cache.delete(oldest);
}
const entry: CacheEntry<TData> = { data, timestamp: new Date(), expiresAt: Date.now() + ttl };
cache.set(key, entry);
inflight.delete(key);
return entry;
})
.catch((err) => {
inflight.delete(key);
throw err;
});
inflight.set(key, promise);
return promise;
},
}),
};
};
inflight.set(key, promise as Promise<CacheEntry<unknown>>);
return promise;
},
}),
});
+12 -12
View File
@@ -732,8 +732,8 @@
"displayType": "",
"jsonPath": "",
"jsonPathHint": "",
"label": "Étiquette",
"unit": "Unité",
"label": "",
"unit": "",
"mappings": "",
"tablePath": "",
"tablePathHint": "",
@@ -755,14 +755,14 @@
"striped": "",
"compact": "",
"maxHeight": "",
"color": "Couleur",
"color": "",
"buttonLabel": "",
"buttonColor": "",
"confirmText": "Texte de confirmation",
"confirmText": "",
"confirmTextHint": "",
"successMessage": "Message de réussite",
"successMessage": "",
"successMessageHint": "",
"enabled": "Activé",
"enabled": "",
"template": {
"label": "",
"description": ""
@@ -1017,13 +1017,13 @@
"timetable": "",
"photoService": "",
"documents": "",
"notes": "Notes",
"mediaMonitoring": "Supervision",
"speedtest": "Test de débit",
"notes": "",
"mediaMonitoring": "",
"speedtest": "",
"analytics": "",
"vpn": "",
"mediaLibrary": "Bibliothèque de médias",
"uptimeMonitoring": "Surveillance des disponibilités"
"mediaLibrary": "",
"uptimeMonitoring": ""
},
"testConnection": {
"action": {
@@ -1298,7 +1298,7 @@
"newLabel": "Nouvelle clé privée"
},
"slug": {
"label": "Slug de la page de statut",
"label": "",
"newLabel": ""
}
}
+11 -11
View File
@@ -1599,7 +1599,7 @@
"menu": {
"label": {
"settings": "Impostazioni",
"refresh": "Aggiorna",
"refresh": "",
"options": "Opzioni",
"actions": "Azioni"
}
@@ -1904,7 +1904,7 @@
"noStation": "Nessuna stazione selezionata",
"option": {
"baseUrl": {
"label": "Url Search.ch"
"label": ""
},
"station": {
"label": "Stazione"
@@ -2791,8 +2791,8 @@
"description": "Visualizza la data dell'immagine e l'indice corrente nella parte inferiore della presentazione"
},
"randomizePhotos": {
"label": "Immagini casuali",
"description": "Mostra le foto in ordine casuale invece di iniziare sempre con lo stesso ordine degli album."
"label": "",
"description": ""
}
},
"description": "Presentazione delle immagini nel tuo album Immich",
@@ -3158,7 +3158,7 @@
"label": "Provider"
},
"providerUrl": {
"label": "URL Provider"
"label": ""
},
"identifier": {
"label": "Identificatore",
@@ -3209,10 +3209,10 @@
"tooltip": "Impossibile analizzare il provider, impostarlo manualmente dopo aver importato le immagini"
},
"tokens": {
"label": "Token Provider",
"token": "Token",
"configured": "Token configurato",
"notConfigured": "Nessun token impostato",
"label": "",
"token": "",
"configured": "",
"notConfigured": "",
"save": "Salva"
}
}
@@ -3236,9 +3236,9 @@
"invalidIdentifier": "Identificatore non valido",
"noMatchingVersion": "Nessuna versione corrispondente trovata",
"noReleasesFound": "Nessun rilascio trovato",
"noProviderSelected": "Nessun provider selezionato",
"noProviderSelected": "",
"noProviderResponse": "Nessuna risposta dal provider",
"unexpected": "Errore provider inaspettato"
"unexpected": ""
}
}
},
+3 -3
View File
@@ -3132,7 +3132,7 @@
"label": ""
},
"showOnlyIcon": {
"label": "Показати тільки іконку"
"label": ""
},
"topReleases": {
"label": "",
@@ -3229,7 +3229,7 @@
"openReleasePage": "",
"releaseDescription": "",
"projectDescription": "",
"created": "Створено",
"created": "",
"error": {
"label": "",
"messages": {
@@ -3350,7 +3350,7 @@
"label": ""
}
},
"latestResult": "Останній результат",
"latestResult": "",
"averages": "",
"tests": "",
"recentResults": "",
@@ -50,7 +50,6 @@ import {
} from "@homarr/definitions";
import { findBestIconMatch, IconPicker } from "@homarr/forms-collection";
import { createModal, useModalAction } from "@homarr/modals";
import { showErrorNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
import { MaskedImage } from "@homarr/ui";
@@ -270,24 +269,8 @@ const providersWithAuth: ReleaseProviderKind[] = [
const ProviderTokensSection = ({ itemId, repositories }: { itemId: string; repositories: ReleasesRepository[] }) => {
const tRepository = useScopedI18n("widget.releases.option.repositories");
const { data: configuredKinds = [], refetch } = clientApi.widget.secrets.getConfiguredKinds.useQuery({ itemId });
const setSecret = clientApi.widget.secrets.setSecret.useMutation({
onSuccess: () => refetch(),
onError: (error) => {
showErrorNotification({
title: "Failed to save token",
message: error.message,
});
},
});
const deleteSecret = clientApi.widget.secrets.deleteSecret.useMutation({
onSuccess: () => refetch(),
onError: (error) => {
showErrorNotification({
title: "Failed to delete token",
message: error.message,
});
},
});
const setSecret = clientApi.widget.secrets.setSecret.useMutation({ onSuccess: () => refetch() });
const deleteSecret = clientApi.widget.secrets.deleteSecret.useMutation({ onSuccess: () => refetch() });
const authProviders = useMemo(() => {
const usedProviders = repositories.map((r) => r.provider).filter((p): p is ReleaseProviderKind => p !== undefined);
+175 -167
View File
@@ -142,14 +142,14 @@ catalogs:
specifier: ^5.101.0
version: 5.101.0
'@testcontainers/mysql':
specifier: ^12.0.4
version: 12.0.4
specifier: ^12.0.3
version: 12.0.3
'@testcontainers/postgresql':
specifier: ^12.0.4
version: 12.0.4
specifier: ^12.0.3
version: 12.0.3
'@testcontainers/redis':
specifier: ^12.0.4
version: 12.0.4
specifier: ^12.0.3
version: 12.0.3
'@tiptap/extension-color':
specifier: 3.27.1
version: 3.27.1
@@ -442,8 +442,8 @@ catalogs:
specifier: ^1.18.0
version: 1.18.0
posthog-js:
specifier: ^1.396.2
version: 1.396.3
specifier: ^1.395.0
version: 1.395.0
posthog-node:
specifier: ^4.18.0
version: 4.18.0
@@ -496,8 +496,8 @@ catalogs:
specifier: ^5.3.1
version: 5.3.1
testcontainers:
specifier: ^12.0.4
version: 12.0.4
specifier: ^12.0.3
version: 12.0.3
timezones-ical-library:
specifier: ^2.2.1
version: 2.2.1
@@ -520,8 +520,8 @@ catalogs:
specifier: ^6.0.3
version: 6.0.3
undici:
specifier: 7.28.0
version: 7.28.0
specifier: 7.27.2
version: 7.27.2
use-deep-compare-effect:
specifier: ^1.8.1
version: 1.8.1
@@ -615,7 +615,7 @@ importers:
version: 14.1.1(semantic-release@25.0.5(typescript@6.0.3))
'@testcontainers/redis':
specifier: 'catalog:'
version: 12.0.4
version: 12.0.3
'@turbo/gen':
specifier: 'catalog:'
version: 2.10.0(@types/node@24.13.2)
@@ -651,7 +651,7 @@ importers:
version: 25.0.5(typescript@6.0.3)
testcontainers:
specifier: 'catalog:'
version: 12.0.4
version: 12.0.3
turbo:
specifier: 'catalog:'
version: 2.10.0
@@ -702,7 +702,7 @@ importers:
version: 0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@scalar/api-reference-react':
specifier: 'catalog:'
version: 0.9.49(nprogress@0.2.0)(react@19.2.7)(tailwindcss@4.3.2)(typescript@6.0.3)(zod@4.4.3)
version: 0.9.49(nprogress@0.2.0)(react@19.2.7)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3)
'@signalwire/docusaurus-plugin-llms-txt':
specifier: ^1.2.2
version: 1.2.2(@docusaurus/core@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.16))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))
@@ -710,8 +710,8 @@ importers:
specifier: 'catalog:'
version: 3.44.0(react@19.2.7)
'@tailwindcss/postcss':
specifier: ^4.3.2
version: 4.3.2
specifier: ^4.3.1
version: 4.3.1
clsx:
specifier: 'catalog:'
version: 2.1.1
@@ -746,8 +746,8 @@ importers:
specifier: ^12.2.0
version: 12.2.0
tailwindcss:
specifier: ^4.3.2
version: 4.3.2
specifier: ^4.3.1
version: 4.3.1
devDependencies:
'@docusaurus/module-type-aliases':
specifier: ^3.10.1
@@ -913,7 +913,7 @@ importers:
version: 1.29.0(zod@4.4.3)
'@scalar/api-reference-react':
specifier: 'catalog:'
version: 0.9.49(nprogress@0.2.0)(react@19.2.7)(tailwindcss@4.3.2)(typescript@6.0.3)(zod@4.4.3)
version: 0.9.49(nprogress@0.2.0)(react@19.2.7)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3)
'@tabler/icons-react':
specifier: 'catalog:'
version: 3.44.0(react@19.2.7)
@@ -1012,7 +1012,7 @@ importers:
version: 1.18.0(postcss@8.5.15)
posthog-js:
specifier: 'catalog:'
version: 1.396.3
version: 1.395.0
prismjs:
specifier: 'catalog:'
version: 1.30.0
@@ -1151,7 +1151,7 @@ importers:
version: 2.2.6
undici:
specifier: 'catalog:'
version: 7.28.0
version: 7.27.2
devDependencies:
'@homarr/tsconfig':
specifier: workspace:^0.1.0
@@ -1500,7 +1500,7 @@ importers:
version: 19.2.7(react@19.2.7)
undici:
specifier: 'catalog:'
version: 7.28.0
version: 7.27.2
zod:
specifier: 'catalog:'
version: 4.4.3
@@ -1688,10 +1688,10 @@ importers:
version: 3.3.0
'@testcontainers/mysql':
specifier: 'catalog:'
version: 12.0.4
version: 12.0.3
'@testcontainers/postgresql':
specifier: 'catalog:'
version: 12.0.4
version: 12.0.3
better-sqlite3:
specifier: 'catalog:'
version: 12.10.0
@@ -1923,7 +1923,7 @@ importers:
version: link:../image-proxy
'@homarr/node-unifi':
specifier: 'catalog:'
version: 2.6.0(undici@7.28.0)
version: 2.6.0(undici@7.27.2)
'@homarr/redis':
specifier: workspace:^0.1.0
version: link:../redis
@@ -1959,7 +1959,7 @@ importers:
version: 2.2.2
undici:
specifier: 'catalog:'
version: 7.28.0
version: 7.27.2
ws:
specifier: 'catalog:'
version: 8.21.0
@@ -2264,7 +2264,7 @@ importers:
version: 2.2.6
undici:
specifier: 'catalog:'
version: 7.28.0
version: 7.27.2
zod:
specifier: 'catalog:'
version: 4.4.3
@@ -6400,11 +6400,11 @@ packages:
'@polka/url@1.0.0-next.25':
resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==}
'@posthog/core@1.39.2':
resolution: {integrity: sha512-G/TL5Xykl+9xCDtBwHHPRSToS/43UjzyxKOfhlTwu/hJHt3v39Q2OFu8hdum71Iu7+tuxxISGVcFMlIST7ksjA==}
'@posthog/core@1.38.0':
resolution: {integrity: sha512-QLrJh0hMVpEJXHNyiJR9YhvIO5tzLDedw4UHdvB+ub4fXHMtHCA8H44qgl11HWR3ajpjxtJ8hs3HyOGVF3Zc1w==}
'@posthog/types@1.392.0':
resolution: {integrity: sha512-nctNujXL3FC1v99FktaTMSugSD9ZOZekEpahUSafkU2TSvW+XGKNkQZbokuJtiWvPBK208dwMJva8UfBkChqpw==}
'@posthog/types@1.391.1':
resolution: {integrity: sha512-ASwd7Nf4pViqdYRYaNRyPYRVKWa1CcHUAUWR0XeQJLGdNnsWACBwe0sSieb/cHnKsRXjRwO/23KIY83lm/Ccpw==}
'@protobufjs/aspromise@1.1.2':
resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
@@ -7172,69 +7172,69 @@ packages:
'@tabler/icons@3.44.0':
resolution: {integrity: sha512-Wn0AOZG9sg0L+bjfMqq4eNhC6pQjIrk94LvvWYNYkY8KH8wC3YILRzQlrnVJc4FUeMxH/AK97QsYCX35H3LndA==}
'@tailwindcss/node@4.3.2':
resolution: {integrity: sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==}
'@tailwindcss/node@4.3.1':
resolution: {integrity: sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==}
'@tailwindcss/oxide-android-arm64@4.3.2':
resolution: {integrity: sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==}
'@tailwindcss/oxide-android-arm64@4.3.1':
resolution: {integrity: sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [android]
'@tailwindcss/oxide-darwin-arm64@4.3.2':
resolution: {integrity: sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==}
'@tailwindcss/oxide-darwin-arm64@4.3.1':
resolution: {integrity: sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [darwin]
'@tailwindcss/oxide-darwin-x64@4.3.2':
resolution: {integrity: sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==}
'@tailwindcss/oxide-darwin-x64@4.3.1':
resolution: {integrity: sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==}
engines: {node: '>= 20'}
cpu: [x64]
os: [darwin]
'@tailwindcss/oxide-freebsd-x64@4.3.2':
resolution: {integrity: sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==}
'@tailwindcss/oxide-freebsd-x64@4.3.1':
resolution: {integrity: sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==}
engines: {node: '>= 20'}
cpu: [x64]
os: [freebsd]
'@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2':
resolution: {integrity: sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==}
'@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1':
resolution: {integrity: sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==}
engines: {node: '>= 20'}
cpu: [arm]
os: [linux]
'@tailwindcss/oxide-linux-arm64-gnu@4.3.2':
resolution: {integrity: sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==}
'@tailwindcss/oxide-linux-arm64-gnu@4.3.1':
resolution: {integrity: sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.3.2':
resolution: {integrity: sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==}
'@tailwindcss/oxide-linux-arm64-musl@4.3.1':
resolution: {integrity: sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.3.2':
resolution: {integrity: sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==}
'@tailwindcss/oxide-linux-x64-gnu@4.3.1':
resolution: {integrity: sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.3.2':
resolution: {integrity: sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==}
'@tailwindcss/oxide-linux-x64-musl@4.3.1':
resolution: {integrity: sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.3.2':
resolution: {integrity: sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==}
'@tailwindcss/oxide-wasm32-wasi@4.3.1':
resolution: {integrity: sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
bundledDependencies:
@@ -7245,24 +7245,24 @@ packages:
- '@emnapi/wasi-threads'
- tslib
'@tailwindcss/oxide-win32-arm64-msvc@4.3.2':
resolution: {integrity: sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==}
'@tailwindcss/oxide-win32-arm64-msvc@4.3.1':
resolution: {integrity: sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [win32]
'@tailwindcss/oxide-win32-x64-msvc@4.3.2':
resolution: {integrity: sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==}
'@tailwindcss/oxide-win32-x64-msvc@4.3.1':
resolution: {integrity: sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==}
engines: {node: '>= 20'}
cpu: [x64]
os: [win32]
'@tailwindcss/oxide@4.3.2':
resolution: {integrity: sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==}
'@tailwindcss/oxide@4.3.1':
resolution: {integrity: sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==}
engines: {node: '>= 20'}
'@tailwindcss/postcss@4.3.2':
resolution: {integrity: sha512-rjVWYCa7Ngbi5AarT6k8TkxUG3Wl1QKzHdIZVsjZSzf36Jmo2IKZt/NHRAwly8oDkbBOH0YTu+CHuf9jPxMc+g==}
'@tailwindcss/postcss@4.3.1':
resolution: {integrity: sha512-dNJuNbdEJT/SWRuXTYP1WSamelsz3ztkUsdtWQPjrexysrTpaEPM40P/71knXiXLYEojqPOEGitVLLpPMS5T6A==}
'@tanstack/match-sorter-utils@8.19.4':
resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==}
@@ -7332,14 +7332,14 @@ packages:
peerDependencies:
vue: ^2.7.0 || ^3.0.0
'@testcontainers/mysql@12.0.4':
resolution: {integrity: sha512-XZWh/L6EN8XQa5lsBdESDolDmyUA56bDgYh5SicLzzC2b4jho4lc0yA0GOOVX/bd0zK6VQK6hmDDJE+T3rFrdA==}
'@testcontainers/mysql@12.0.3':
resolution: {integrity: sha512-xaub3gaAQKR4Q2/9d8B+81XM8LLTQHPOncCL3Ved04JUUIeTobMt3J36JXv/rvEpnrMpphlKYlmQU0aq48HeZA==}
'@testcontainers/postgresql@12.0.4':
resolution: {integrity: sha512-a/pLU6j5lpKKAlUTPwqweqMGhOSjgTSb6HBX69TOrXn32ifU37nnQDmNFTj8ddOAw+BQL9oTRkeOxVbZkqhgZA==}
'@testcontainers/postgresql@12.0.3':
resolution: {integrity: sha512-y0l/ItWxNfdMU783EOZ66lMq90uuuXPqE+fo1jNSRfMbtfUq/9Ez/wluDxxaS+D/i3I0juep+jmz/WVuip8i9Q==}
'@testcontainers/redis@12.0.4':
resolution: {integrity: sha512-44Ov3Lcptb3KqljcCy6xIJ2VxNsN+3Kw/YpBiotZpOcI4iLgLNmcOgvCFPVo+vDoh4rCfgtNUEbIpL5Mu7oRuw==}
'@testcontainers/redis@12.0.3':
resolution: {integrity: sha512-hN3YedOSb6quFzG1IM4j/r/M635GcaYOxzbTuiSN7676BZHrx1VPl+xll3ojk8ds6TvenrfO37lrEVJYN2URlA==}
'@tiptap/core@3.27.1':
resolution: {integrity: sha512-rV6Qn4wmC6BxfF+4mu6bqGWj9vA4oXXhsrpXaJL2uhjxeHAGofjwcHof2X84VYzeyXgdlsGmqKie4TAppVXZUQ==}
@@ -9673,6 +9673,10 @@ packages:
resolution: {integrity: sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==}
engines: {node: '>= 8.0'}
dockerode@5.0.0:
resolution: {integrity: sha512-C52mvJ+7lcyhWNfrzVfFsbTrBfy/ezE9FGEYLpu17FUeBcCkxERk9nN7uDl/478ynDiQ4U+5DbQC2vENHkVEtQ==}
engines: {node: '>= 14.17'}
dockerode@5.0.1:
resolution: {integrity: sha512-avsq/xk4YPIrn0CgleX5bjT9Y8IT1p9PxrNQ++RBQ2WEyFfHCTDsT9kmyxz+H/axnjAwg8wJWEIuPGOUuNupiA==}
engines: {node: '>= 14.17'}
@@ -10420,9 +10424,9 @@ packages:
get-own-enumerable-property-symbols@3.0.2:
resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==}
get-port@5.1.1:
resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==}
engines: {node: '>=8'}
get-port@7.2.0:
resolution: {integrity: sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg==}
engines: {node: '>=16'}
get-proto@1.0.1:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
@@ -11177,7 +11181,6 @@ packages:
jiti@2.7.0:
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
hasBin: true
joi@17.13.3:
resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==}
@@ -13112,8 +13115,8 @@ packages:
resolution: {integrity: sha512-Ray65LYEJrMMqDtsBUBXunEVP/g4wtATvq/xz9rchUoLy/9mSkkFgUko/8DVtGxgiP3vivpFMgfb9HpCuDrBHg==}
engines: {node: '>=10.15.1'}
posthog-js@1.396.3:
resolution: {integrity: sha512-CL4mH3F2azsONko/zC2931RLdt6RxTsEr7x7Lyk0SDmj5hZuvBCTbg9MKViXwSRhBr3/SaGcaHS1n95WxTrsCw==}
posthog-js@1.395.0:
resolution: {integrity: sha512-5iTb00CGt2eQUUiBQysQiX89RAbCN6wK2sDNzvs9zv0alaY8mJ0ZySrUD3LQ+XyLhgM5pCpacBuUwChqiYDLDw==}
posthog-node@4.18.0:
resolution: {integrity: sha512-XROs1h+DNatgKh/AlIlCtDxWzwrKdYDb2mOs58n4yN8BkGN9ewqeQwG5ApS4/IzwCb7HPttUkOVulkYatd2PIw==}
@@ -14291,8 +14294,8 @@ packages:
tailwind-merge@3.5.0:
resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==}
tailwindcss@4.3.2:
resolution: {integrity: sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==}
tailwindcss@4.3.1:
resolution: {integrity: sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==}
tapable@2.3.3:
resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
@@ -14369,8 +14372,8 @@ packages:
resolution: {integrity: sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==}
engines: {node: '>=10'}
testcontainers@12.0.4:
resolution: {integrity: sha512-QIR/8xF1+F/26cIM+9B4yyxNTbKJxAv3hygZyhPRgZ8Q2AhlPZjDdpXRuk16V37X4bgJRI3hXFhoEICMBA7Adg==}
testcontainers@12.0.3:
resolution: {integrity: sha512-6vfiL9CWIX0rWhTJitzrzHcv4Q/sUfUdBV12jMz6HZ58Lz6ZtXVecO6jljd1O3ookEWQHNICiYoYNEPpDLzuRw==}
text-decoder@1.2.0:
resolution: {integrity: sha512-n1yg1mOj9DNpk3NeZOx7T6jchTbyJS3i3cucbNN6FcdPriMZx7NsgrGpWWdWZZGxD7ES1XB+3uoqHMgOKaN+fg==}
@@ -14662,12 +14665,8 @@ packages:
resolution: {integrity: sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==}
engines: {node: '>=20.18.1'}
undici@7.28.0:
resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==}
engines: {node: '>=20.18.1'}
undici@8.5.0:
resolution: {integrity: sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==}
undici@8.4.1:
resolution: {integrity: sha512-RNHlB4fxZK0IrkhBsxhlbx7s8kFWwr7rzzOqj5nvZugw3ig3RsB7KW3zVlV0eu8POl+rx5d1hmL7rRg0z1owow==}
engines: {node: '>=22.19.0'}
unhead@2.1.15:
@@ -18413,9 +18412,9 @@ snapshots:
dependencies:
'@hapi/hoek': 9.3.0
'@headlessui/tailwindcss@0.2.2(tailwindcss@4.3.2)':
'@headlessui/tailwindcss@0.2.2(tailwindcss@4.3.1)':
dependencies:
tailwindcss: 4.3.2
tailwindcss: 4.3.1
'@headlessui/vue@1.7.23(vue@3.5.35(typescript@6.0.3))':
dependencies:
@@ -18424,11 +18423,11 @@ snapshots:
'@homarr/gridstack@1.12.0': {}
'@homarr/node-unifi@2.6.0(undici@7.28.0)':
'@homarr/node-unifi@2.6.0(undici@7.27.2)':
dependencies:
axios: 1.9.0
eventemitter2: 6.4.9
http-cookie-agent: 6.0.8(tough-cookie@5.1.2)(undici@7.28.0)
http-cookie-agent: 6.0.8(tough-cookie@5.1.2)(undici@7.27.2)
tough-cookie: 5.1.2
ws: 8.21.0
transitivePeerDependencies:
@@ -19876,11 +19875,11 @@ snapshots:
'@polka/url@1.0.0-next.25': {}
'@posthog/core@1.39.2':
'@posthog/core@1.38.0':
dependencies:
'@posthog/types': 1.392.0
'@posthog/types': 1.391.1
'@posthog/types@1.392.0': {}
'@posthog/types@1.391.1': {}
'@protobufjs/aspromise@1.1.2': {}
@@ -20095,11 +20094,11 @@ snapshots:
'@rvf/set-get@7.0.1': {}
'@scalar/agent-chat@0.12.13(nprogress@0.2.0)(tailwindcss@4.3.2)(typescript@6.0.3)(zod@4.4.3)':
'@scalar/agent-chat@0.12.13(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3)':
dependencies:
'@ai-sdk/vue': 3.0.33(vue@3.5.35(typescript@6.0.3))(zod@4.4.3)
'@scalar/api-client': 3.12.0(nprogress@0.2.0)(tailwindcss@4.3.2)(typescript@6.0.3)
'@scalar/components': 0.27.4(tailwindcss@4.3.2)(typescript@6.0.3)
'@scalar/api-client': 3.12.0(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/components': 0.27.4(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/helpers': 0.9.0
'@scalar/icons': 0.7.3(typescript@6.0.3)
'@scalar/json-magic': 0.12.17
@@ -20133,17 +20132,17 @@ snapshots:
- universal-cookie
- zod
'@scalar/api-client@3.12.0(nprogress@0.2.0)(tailwindcss@4.3.2)(typescript@6.0.3)':
'@scalar/api-client@3.12.0(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)':
dependencies:
'@headlessui/tailwindcss': 0.2.2(tailwindcss@4.3.2)
'@headlessui/tailwindcss': 0.2.2(tailwindcss@4.3.1)
'@headlessui/vue': 1.7.23(vue@3.5.35(typescript@6.0.3))
'@scalar/blocks': 0.1.0(tailwindcss@4.3.2)(typescript@6.0.3)
'@scalar/components': 0.27.4(tailwindcss@4.3.2)(typescript@6.0.3)
'@scalar/blocks': 0.1.0(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/components': 0.27.4(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/helpers': 0.9.0
'@scalar/icons': 0.7.3(typescript@6.0.3)
'@scalar/oas-utils': 0.19.1(typescript@6.0.3)
'@scalar/openapi-types': 0.9.1
'@scalar/sidebar': 0.9.25(tailwindcss@4.3.2)(typescript@6.0.3)
'@scalar/sidebar': 0.9.25(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/snippetz': 0.9.19
'@scalar/themes': 0.16.2
'@scalar/typebox': 0.1.3
@@ -20181,9 +20180,9 @@ snapshots:
- typescript
- universal-cookie
'@scalar/api-reference-react@0.9.49(nprogress@0.2.0)(react@19.2.7)(tailwindcss@4.3.2)(typescript@6.0.3)(zod@4.4.3)':
'@scalar/api-reference-react@0.9.49(nprogress@0.2.0)(react@19.2.7)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3)':
dependencies:
'@scalar/api-reference': 1.62.0(nprogress@0.2.0)(tailwindcss@4.3.2)(typescript@6.0.3)(zod@4.4.3)
'@scalar/api-reference': 1.62.0(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3)
'@scalar/types': 0.16.0
react: 19.2.7
transitivePeerDependencies:
@@ -20203,19 +20202,19 @@ snapshots:
- universal-cookie
- zod
'@scalar/api-reference@1.62.0(nprogress@0.2.0)(tailwindcss@4.3.2)(typescript@6.0.3)(zod@4.4.3)':
'@scalar/api-reference@1.62.0(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3)':
dependencies:
'@headlessui/vue': 1.7.23(vue@3.5.35(typescript@6.0.3))
'@scalar/agent-chat': 0.12.13(nprogress@0.2.0)(tailwindcss@4.3.2)(typescript@6.0.3)(zod@4.4.3)
'@scalar/api-client': 3.12.0(nprogress@0.2.0)(tailwindcss@4.3.2)(typescript@6.0.3)
'@scalar/blocks': 0.1.0(tailwindcss@4.3.2)(typescript@6.0.3)
'@scalar/agent-chat': 0.12.13(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3)
'@scalar/api-client': 3.12.0(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/blocks': 0.1.0(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/code-highlight': 0.4.0
'@scalar/components': 0.27.4(tailwindcss@4.3.2)(typescript@6.0.3)
'@scalar/components': 0.27.4(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/helpers': 0.9.0
'@scalar/icons': 0.7.3(typescript@6.0.3)
'@scalar/oas-utils': 0.19.1(typescript@6.0.3)
'@scalar/schemas': 0.7.0
'@scalar/sidebar': 0.9.25(tailwindcss@4.3.2)(typescript@6.0.3)
'@scalar/sidebar': 0.9.25(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/snippetz': 0.9.19
'@scalar/themes': 0.16.2
'@scalar/types': 0.16.0
@@ -20247,9 +20246,9 @@ snapshots:
- universal-cookie
- zod
'@scalar/blocks@0.1.0(tailwindcss@4.3.2)(typescript@6.0.3)':
'@scalar/blocks@0.1.0(tailwindcss@4.3.1)(typescript@6.0.3)':
dependencies:
'@scalar/components': 0.27.4(tailwindcss@4.3.2)(typescript@6.0.3)
'@scalar/components': 0.27.4(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/helpers': 0.9.0
'@scalar/icons': 0.7.3(typescript@6.0.3)
'@scalar/snippetz': 0.9.19
@@ -20285,11 +20284,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@scalar/components@0.27.4(tailwindcss@4.3.2)(typescript@6.0.3)':
'@scalar/components@0.27.4(tailwindcss@4.3.1)(typescript@6.0.3)':
dependencies:
'@floating-ui/utils': 0.2.10
'@floating-ui/vue': 1.1.9(vue@3.5.35(typescript@6.0.3))
'@headlessui/tailwindcss': 0.2.2(tailwindcss@4.3.2)
'@headlessui/tailwindcss': 0.2.2(tailwindcss@4.3.1)
'@headlessui/vue': 1.7.23(vue@3.5.35(typescript@6.0.3))
'@scalar/code-highlight': 0.4.0
'@scalar/helpers': 0.9.0
@@ -20347,9 +20346,9 @@ snapshots:
'@scalar/helpers': 0.9.0
'@scalar/validation': 0.6.0
'@scalar/sidebar@0.9.25(tailwindcss@4.3.2)(typescript@6.0.3)':
'@scalar/sidebar@0.9.25(tailwindcss@4.3.1)(typescript@6.0.3)':
dependencies:
'@scalar/components': 0.27.4(tailwindcss@4.3.2)(typescript@6.0.3)
'@scalar/components': 0.27.4(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/helpers': 0.9.0
'@scalar/icons': 0.7.3(typescript@6.0.3)
'@scalar/themes': 0.16.2
@@ -20826,7 +20825,7 @@ snapshots:
'@tabler/icons@3.44.0': {}
'@tailwindcss/node@4.3.2':
'@tailwindcss/node@4.3.1':
dependencies:
'@jridgewell/remapping': 2.3.5
enhanced-resolve: 5.21.6
@@ -20834,66 +20833,66 @@ snapshots:
lightningcss: 1.32.0
magic-string: 0.30.21
source-map-js: 1.2.1
tailwindcss: 4.3.2
tailwindcss: 4.3.1
'@tailwindcss/oxide-android-arm64@4.3.2':
'@tailwindcss/oxide-android-arm64@4.3.1':
optional: true
'@tailwindcss/oxide-darwin-arm64@4.3.2':
'@tailwindcss/oxide-darwin-arm64@4.3.1':
optional: true
'@tailwindcss/oxide-darwin-x64@4.3.2':
'@tailwindcss/oxide-darwin-x64@4.3.1':
optional: true
'@tailwindcss/oxide-freebsd-x64@4.3.2':
'@tailwindcss/oxide-freebsd-x64@4.3.1':
optional: true
'@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2':
'@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1':
optional: true
'@tailwindcss/oxide-linux-arm64-gnu@4.3.2':
'@tailwindcss/oxide-linux-arm64-gnu@4.3.1':
optional: true
'@tailwindcss/oxide-linux-arm64-musl@4.3.2':
'@tailwindcss/oxide-linux-arm64-musl@4.3.1':
optional: true
'@tailwindcss/oxide-linux-x64-gnu@4.3.2':
'@tailwindcss/oxide-linux-x64-gnu@4.3.1':
optional: true
'@tailwindcss/oxide-linux-x64-musl@4.3.2':
'@tailwindcss/oxide-linux-x64-musl@4.3.1':
optional: true
'@tailwindcss/oxide-wasm32-wasi@4.3.2':
'@tailwindcss/oxide-wasm32-wasi@4.3.1':
optional: true
'@tailwindcss/oxide-win32-arm64-msvc@4.3.2':
'@tailwindcss/oxide-win32-arm64-msvc@4.3.1':
optional: true
'@tailwindcss/oxide-win32-x64-msvc@4.3.2':
'@tailwindcss/oxide-win32-x64-msvc@4.3.1':
optional: true
'@tailwindcss/oxide@4.3.2':
'@tailwindcss/oxide@4.3.1':
optionalDependencies:
'@tailwindcss/oxide-android-arm64': 4.3.2
'@tailwindcss/oxide-darwin-arm64': 4.3.2
'@tailwindcss/oxide-darwin-x64': 4.3.2
'@tailwindcss/oxide-freebsd-x64': 4.3.2
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.2
'@tailwindcss/oxide-linux-arm64-gnu': 4.3.2
'@tailwindcss/oxide-linux-arm64-musl': 4.3.2
'@tailwindcss/oxide-linux-x64-gnu': 4.3.2
'@tailwindcss/oxide-linux-x64-musl': 4.3.2
'@tailwindcss/oxide-wasm32-wasi': 4.3.2
'@tailwindcss/oxide-win32-arm64-msvc': 4.3.2
'@tailwindcss/oxide-win32-x64-msvc': 4.3.2
'@tailwindcss/oxide-android-arm64': 4.3.1
'@tailwindcss/oxide-darwin-arm64': 4.3.1
'@tailwindcss/oxide-darwin-x64': 4.3.1
'@tailwindcss/oxide-freebsd-x64': 4.3.1
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.1
'@tailwindcss/oxide-linux-arm64-gnu': 4.3.1
'@tailwindcss/oxide-linux-arm64-musl': 4.3.1
'@tailwindcss/oxide-linux-x64-gnu': 4.3.1
'@tailwindcss/oxide-linux-x64-musl': 4.3.1
'@tailwindcss/oxide-wasm32-wasi': 4.3.1
'@tailwindcss/oxide-win32-arm64-msvc': 4.3.1
'@tailwindcss/oxide-win32-x64-msvc': 4.3.1
'@tailwindcss/postcss@4.3.2':
'@tailwindcss/postcss@4.3.1':
dependencies:
'@alloc/quick-lru': 5.2.0
'@tailwindcss/node': 4.3.2
'@tailwindcss/oxide': 4.3.2
postcss: 8.5.16
tailwindcss: 4.3.2
'@tailwindcss/node': 4.3.1
'@tailwindcss/oxide': 4.3.1
postcss: 8.5.15
tailwindcss: 4.3.1
'@tanstack/match-sorter-utils@8.19.4':
dependencies:
@@ -20958,23 +20957,23 @@ snapshots:
'@tanstack/virtual-core': 3.17.0
vue: 3.5.35(typescript@6.0.3)
'@testcontainers/mysql@12.0.4':
'@testcontainers/mysql@12.0.3':
dependencies:
testcontainers: 12.0.4
testcontainers: 12.0.3
transitivePeerDependencies:
- bare-buffer
- supports-color
'@testcontainers/postgresql@12.0.4':
'@testcontainers/postgresql@12.0.3':
dependencies:
testcontainers: 12.0.4
testcontainers: 12.0.3
transitivePeerDependencies:
- bare-buffer
- supports-color
'@testcontainers/redis@12.0.4':
'@testcontainers/redis@12.0.3':
dependencies:
testcontainers: 12.0.4
testcontainers: 12.0.3
transitivePeerDependencies:
- bare-buffer
- supports-color
@@ -23470,6 +23469,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
dockerode@5.0.0:
dependencies:
'@balena/dockerignore': 1.0.2
'@grpc/grpc-js': 1.12.5
'@grpc/proto-loader': 0.7.13
docker-modem: 5.0.7
protobufjs: 7.4.0
tar-fs: 2.1.4
transitivePeerDependencies:
- supports-color
dockerode@5.0.1:
dependencies:
'@balena/dockerignore': 1.0.2
@@ -24327,7 +24337,7 @@ snapshots:
get-own-enumerable-property-symbols@3.0.2: {}
get-port@5.1.1: {}
get-port@7.2.0: {}
get-proto@1.0.1:
dependencies:
@@ -24830,12 +24840,12 @@ snapshots:
http-cache-semantics@4.2.0: {}
http-cookie-agent@6.0.8(tough-cookie@5.1.2)(undici@7.28.0):
http-cookie-agent@6.0.8(tough-cookie@5.1.2)(undici@7.27.2):
dependencies:
agent-base: 7.1.3
tough-cookie: 5.1.2
optionalDependencies:
undici: 7.28.0
undici: 7.27.2
http-deceiver@1.2.7: {}
@@ -27409,12 +27419,12 @@ snapshots:
posthog-docusaurus@2.0.5: {}
posthog-js@1.396.3:
posthog-js@1.395.0:
dependencies:
'@posthog/core': 1.39.2
'@posthog/types': 1.392.0
'@posthog/core': 1.38.0
'@posthog/types': 1.391.1
core-js: 3.49.0
dompurify: 3.4.11
dompurify: 3.4.10
fflate: 0.4.8
preact: 10.29.2
query-selector-shadow-dom: 1.0.1
@@ -28923,7 +28933,7 @@ snapshots:
tailwind-merge@3.5.0: {}
tailwindcss@4.3.2: {}
tailwindcss@4.3.1: {}
tapable@2.3.3: {}
@@ -29039,7 +29049,7 @@ snapshots:
commander: 2.20.3
source-map-support: 0.5.21
testcontainers@12.0.4:
testcontainers@12.0.3:
dependencies:
'@balena/dockerignore': 1.0.2
'@types/dockerode': 4.0.1
@@ -29048,14 +29058,14 @@ snapshots:
byline: 5.0.0
debug: 4.4.3
docker-compose: 1.4.2
dockerode: 5.0.1
get-port: 5.1.1
dockerode: 5.0.0
get-port: 7.2.0
proper-lockfile: 4.1.2
properties-reader: 3.0.1
ssh-remote-port-forward: 1.0.4
tar-fs: 3.1.3
tar-fs: 3.1.2
tmp: 0.2.7
undici: 8.5.0
undici: 8.4.1
transitivePeerDependencies:
- bare-buffer
- supports-color
@@ -29314,9 +29324,7 @@ snapshots:
undici@7.27.2: {}
undici@7.28.0: {}
undici@8.5.0: {}
undici@8.4.1: {}
unhead@2.1.15:
dependencies:
+6 -8
View File
@@ -50,9 +50,9 @@ catalog:
"@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
"@testcontainers/mysql": ^12.0.3
"@testcontainers/postgresql": ^12.0.3
"@testcontainers/redis": ^12.0.3
"@tiptap/extension-color": 3.27.1
"@tiptap/extension-highlight": 3.27.1
"@tiptap/extension-image": 3.27.1
@@ -93,7 +93,7 @@ catalog:
"@types/video.js": ^7.3.58
"@types/ws": ^8.18.1
"@types/xml2js": ^0.4.14
posthog-js: ^1.396.2
posthog-js: ^1.395.0
posthog-node: ^4.18.0
"@vitejs/plugin-react": ^6.0.3
"@vitest/coverage-v8": ^4.1.9
@@ -170,7 +170,7 @@ catalog:
semver-parser: ^4.2.1
superjson: 2.2.6
svgson: ^5.3.1
testcontainers: ^12.0.4
testcontainers: ^12.0.3
timezones-ical-library: ^2.2.1
trpc-to-mcp: ^1.3.2
trpc-to-openapi: ^3.3.0
@@ -179,7 +179,7 @@ catalog:
turbo: ^2.10.0
typescript: ^6.0.3
vite: ^8.1.0
undici: 7.28.0
undici: 7.27.2
use-deep-compare-effect: ^1.8.1
video.js: ^8.23.9
vite-tsconfig-paths: ^6.1.1
@@ -205,8 +205,6 @@ minimumReleaseAgeExclude:
- turbo@2.9.14
# Renovate security update: form-data@4.0.6
- form-data@4.0.6
# Renovate security update: undici@7.28.0
- undici@7.28.0
overrides:
"@babel/helpers@<7.26.10": ">=7.29.7"
"@babel/runtime@<7.26.10": ">=7.29.7"