Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fbdc7ab10 | ||
|
|
3e8a8fdfa1 | ||
|
|
59c084bc57 | ||
|
|
f6cf262bab | ||
|
|
ba860aacaa | ||
|
|
161dafbdb8 | ||
|
|
a382c75968 | ||
|
|
6f47a05cc2 | ||
|
|
d8d5894b28 | ||
|
|
46ae756d15 | ||
|
|
8b63799f1b | ||
|
|
60a69e31ef | ||
|
|
57ef0af4a2 | ||
|
|
597b5fbc8d | ||
|
|
5412577519 |
No files matched your search
@@ -1,3 +1,9 @@
|
|||||||
|
## [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)
|
## [1.69.0](https://github.com/homarr-labs/homarr/compare/v1.68.1...v1.69.0) (2026-07-03)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -71,15 +71,3 @@ API key authentication enables programmatic access for:
|
|||||||
- Third-party integrations
|
- Third-party integrations
|
||||||
- Custom dashboards and monitoring tools
|
- 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.
|
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{"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
|
{"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
|
||||||
@@ -24,6 +24,7 @@ import type { AppRouter } from "@homarr/api";
|
|||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import {
|
import {
|
||||||
isPersistableWidgetQueryKey,
|
isPersistableWidgetQueryKey,
|
||||||
|
queryCacheBuster,
|
||||||
queryCacheDefaultGcTimeMs,
|
queryCacheDefaultGcTimeMs,
|
||||||
queryCacheDefaultRefetchIntervalMs,
|
queryCacheDefaultRefetchIntervalMs,
|
||||||
queryCacheDefaultStaleTimeMs,
|
queryCacheDefaultStaleTimeMs,
|
||||||
@@ -136,6 +137,7 @@ export function TRPCReactProvider(props: PropsWithChildren) {
|
|||||||
client={queryClient}
|
client={queryClient}
|
||||||
persistOptions={{
|
persistOptions={{
|
||||||
persister,
|
persister,
|
||||||
|
buster: queryCacheBuster,
|
||||||
maxAge: queryCacheDefaultGcTimeMs,
|
maxAge: queryCacheDefaultGcTimeMs,
|
||||||
dehydrateOptions: {
|
dehydrateOptions: {
|
||||||
shouldDehydrateQuery: (query) =>
|
shouldDehydrateQuery: (query) =>
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import "~/styles/gridstack.scss";
|
|||||||
|
|
||||||
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
|
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
|
||||||
import type { PersistedClient } from "@tanstack/react-query-persist-client";
|
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 { getQueryClient } from "@homarr/api/server";
|
||||||
import { IntegrationProvider } from "@homarr/auth/client";
|
import { IntegrationProvider } from "@homarr/auth/client";
|
||||||
import { auth } from "@homarr/auth/next";
|
import { auth } from "@homarr/auth/next";
|
||||||
@@ -121,7 +123,8 @@ async function QueryCacheHydration({ userId, boardId }: { userId: string; boardI
|
|||||||
const serialized = await getQueryCacheAsync(userId, boardId);
|
const serialized = await getQueryCacheAsync(userId, boardId);
|
||||||
if (!serialized) return null;
|
if (!serialized) return null;
|
||||||
|
|
||||||
const persisted = JSON.parse(serialized) as PersistedClient;
|
const persisted = superjson.parse<PersistedClient>(serialized);
|
||||||
|
if (persisted.buster !== queryCacheBuster) return null;
|
||||||
if (!persisted?.clientState?.queries?.length) return null;
|
if (!persisted?.clientState?.queries?.length) return null;
|
||||||
|
|
||||||
return <HydrationBoundary state={persisted.clientState} />;
|
return <HydrationBoundary state={persisted.clientState} />;
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homarr",
|
"name": "homarr",
|
||||||
"version": "1.69.0",
|
"version": "1.69.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cross-env CI=true turbo build",
|
"build": "cross-env CI=true turbo build",
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { iconsRouter } from "./router/icons";
|
|||||||
import { infoRouter } from "./router/info";
|
import { infoRouter } from "./router/info";
|
||||||
import { integrationRouter } from "./router/integration/integration-router";
|
import { integrationRouter } from "./router/integration/integration-router";
|
||||||
import { inviteRouter } from "./router/invite";
|
import { inviteRouter } from "./router/invite";
|
||||||
import { serverSettingsRouter } from "./router/serverSettings";
|
|
||||||
import { beszelRouter } from "./router/widgets/beszel";
|
import { beszelRouter } from "./router/widgets/beszel";
|
||||||
import { calendarRouter } from "./router/widgets/calendar";
|
import { calendarRouter } from "./router/widgets/calendar";
|
||||||
import { dnsHoleRouter } from "./router/widgets/dns-hole";
|
import { dnsHoleRouter } from "./router/widgets/dns-hole";
|
||||||
@@ -29,7 +28,6 @@ export const mcpRouter = createTRPCRouter({
|
|||||||
info: infoRouter,
|
info: infoRouter,
|
||||||
integration: integrationRouter,
|
integration: integrationRouter,
|
||||||
invite: inviteRouter,
|
invite: inviteRouter,
|
||||||
serverSettings: serverSettingsRouter,
|
|
||||||
beszel: beszelRouter,
|
beszel: beszelRouter,
|
||||||
calendar: calendarRouter,
|
calendar: calendarRouter,
|
||||||
dnsHole: dnsHoleRouter,
|
dnsHole: dnsHoleRouter,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { appRouter } from "./router/app";
|
|||||||
import { boardRouter } from "./router/board";
|
import { boardRouter } from "./router/board";
|
||||||
import { infoRouter } from "./router/info";
|
import { infoRouter } from "./router/info";
|
||||||
import { inviteRouter } from "./router/invite";
|
import { inviteRouter } from "./router/invite";
|
||||||
import { serverSettingsRouter } from "./router/serverSettings";
|
|
||||||
import { userRouter } from "./router/user";
|
import { userRouter } from "./router/user";
|
||||||
import { createTRPCRouter } from "./trpc";
|
import { createTRPCRouter } from "./trpc";
|
||||||
|
|
||||||
@@ -15,14 +14,13 @@ export const openApiRouter = createTRPCRouter({
|
|||||||
boardRouter,
|
boardRouter,
|
||||||
infoRouter,
|
infoRouter,
|
||||||
inviteRouter,
|
inviteRouter,
|
||||||
serverSettingsRouter,
|
|
||||||
userRouter,
|
userRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const openApiDocument = (base: string) =>
|
export const openApiDocument = (base: string) =>
|
||||||
generateOpenApiDocument(openApiRouter, {
|
generateOpenApiDocument(openApiRouter, {
|
||||||
title: "Homarr API documentation",
|
title: "Homarr API documentation",
|
||||||
version: "1.1.0",
|
version: "1.0.0",
|
||||||
baseUrl: base,
|
baseUrl: base,
|
||||||
docsUrl: "https://homarr.dev",
|
docsUrl: "https://homarr.dev",
|
||||||
securitySchemes: {
|
securitySchemes: {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const queryCacheDefaultRefetchIntervalMs = 30_000;
|
|||||||
export const queryCacheDefaultGcTimeMs = 1000 * 60 * 60 * 24;
|
export const queryCacheDefaultGcTimeMs = 1000 * 60 * 60 * 24;
|
||||||
export const queryCacheMaxValueBytes = 1024 * 1024;
|
export const queryCacheMaxValueBytes = 1024 * 1024;
|
||||||
export const queryCacheStoragePrefix = "homarr-widget-query";
|
export const queryCacheStoragePrefix = "homarr-widget-query";
|
||||||
|
export const queryCacheBuster = "v2-superjson";
|
||||||
|
|
||||||
let activeQueryCacheBoardId: string | null = null;
|
let activeQueryCacheBoardId: string | null = null;
|
||||||
|
|
||||||
|
|||||||
@@ -330,16 +330,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
duplicateBoard: permissionRequiredProcedure
|
duplicateBoard: permissionRequiredProcedure
|
||||||
.requiresPermission("board-create")
|
.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)
|
.input(boardDuplicateSchema)
|
||||||
.output(z.object({ boardId: z.string() }))
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "view");
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "view");
|
||||||
await noBoardWithSimilarNameAsync(ctx.db, input.name);
|
await noBoardWithSimilarNameAsync(ctx.db, input.name);
|
||||||
@@ -553,57 +544,33 @@ export const boardRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return { boardId: newBoardId };
|
|
||||||
}),
|
}),
|
||||||
renameBoard: protectedProcedure
|
renameBoard: protectedProcedure.input(boardRenameSchema).mutation(async ({ ctx, input }) => {
|
||||||
.meta({
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
|
||||||
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));
|
await ctx.db.update(boards).set({ name: input.name }).where(eq(boards.id, input.id));
|
||||||
}),
|
}),
|
||||||
changeBoardVisibility: protectedProcedure
|
changeBoardVisibility: protectedProcedure.input(boardChangeVisibilitySchema).mutation(async ({ ctx, input }) => {
|
||||||
.meta({
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
|
||||||
openapi: { method: "PATCH", path: "/api/boards/{id}/visibility", tags: ["boards"], protect: true },
|
const boardSettings = await getServerSettingByKeyAsync(ctx.db, "board");
|
||||||
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 (
|
if (
|
||||||
input.visibility !== "public" &&
|
input.visibility !== "public" &&
|
||||||
(boardSettings.homeBoardId === input.id || boardSettings.mobileHomeBoardId === input.id)
|
(boardSettings.homeBoardId === input.id || boardSettings.mobileHomeBoardId === input.id)
|
||||||
) {
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Cannot make home board private",
|
message: "Cannot make home board private",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.db
|
await ctx.db
|
||||||
.update(boards)
|
.update(boards)
|
||||||
.set({ isPublic: input.visibility === "public" })
|
.set({ isPublic: input.visibility === "public" })
|
||||||
.where(eq(boards.id, input.id));
|
.where(eq(boards.id, input.id));
|
||||||
}),
|
}),
|
||||||
deleteBoard: protectedProcedure
|
deleteBoard: protectedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
openapi: { method: "DELETE", path: "/api/boards/{id}", tags: ["boards"], protect: true },
|
openapi: { method: "DELETE", path: "/api/boards/{id}", tags: ["boards"], protect: true },
|
||||||
@@ -620,38 +587,16 @@ export const boardRouter = createTRPCRouter({
|
|||||||
|
|
||||||
await ctx.db.delete(boards).where(eq(boards.id, input.id));
|
await ctx.db.delete(boards).where(eq(boards.id, input.id));
|
||||||
}),
|
}),
|
||||||
setHomeBoard: protectedProcedure
|
setHomeBoard: protectedProcedure.input(z.object({ id: z.string() })).mutation(async ({ ctx, input }) => {
|
||||||
.meta({
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "view");
|
||||||
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));
|
await ctx.db.update(users).set({ homeBoardId: input.id }).where(eq(users.id, ctx.session.user.id));
|
||||||
}),
|
}),
|
||||||
setMobileHomeBoard: protectedProcedure
|
setMobileHomeBoard: protectedProcedure.input(z.object({ id: z.string() })).mutation(async ({ ctx, input }) => {
|
||||||
.meta({
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "view");
|
||||||
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 }) => {
|
getHomeBoard: publicProcedure.query(async ({ ctx }) => {
|
||||||
const userId = ctx.session?.user.id;
|
const userId = ctx.session?.user.id;
|
||||||
const user = userId
|
const user = userId
|
||||||
@@ -807,16 +752,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
savePartialBoardSettings: protectedProcedure
|
savePartialBoardSettings: protectedProcedure
|
||||||
.meta({
|
.input(boardSavePartialSettingsSchema.and(z.object({ id: z.string() })))
|
||||||
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 }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { boards } from "@homarr/db/schema";
|
|||||||
import { removeQueryCacheAsync, setQueryCacheAsync } from "@homarr/redis";
|
import { removeQueryCacheAsync, setQueryCacheAsync } from "@homarr/redis";
|
||||||
|
|
||||||
import { queryCacheDefaultGcTimeMs, queryCacheMaxValueBytes } from "../query-cache";
|
import { queryCacheDefaultGcTimeMs, queryCacheMaxValueBytes } from "../query-cache";
|
||||||
import { createTRPCRouter, publicProcedure } from "../trpc";
|
import { createTRPCRouter, internalProcedure } from "../trpc";
|
||||||
import { throwIfActionForbiddenAsync } from "./board/board-access";
|
import { throwIfActionForbiddenAsync } from "./board/board-access";
|
||||||
|
|
||||||
const queryCacheBoardInput = z.object({
|
const queryCacheBoardInput = z.object({
|
||||||
@@ -21,7 +21,7 @@ const validateBoardAccessAsync = async (ctx: Parameters<typeof throwIfActionForb
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const queryCacheRouter = createTRPCRouter({
|
export const queryCacheRouter = createTRPCRouter({
|
||||||
setItem: publicProcedure.input(queryCacheSetInput).mutation(async ({ ctx, input }) => {
|
setItem: internalProcedure.input(queryCacheSetInput).mutation(async ({ ctx, input }) => {
|
||||||
await validateBoardAccessAsync(ctx, input.boardId);
|
await validateBoardAccessAsync(ctx, input.boardId);
|
||||||
const userId = ctx.session?.user.id ?? "anonymous";
|
const userId = ctx.session?.user.id ?? "anonymous";
|
||||||
return {
|
return {
|
||||||
@@ -34,7 +34,7 @@ export const queryCacheRouter = createTRPCRouter({
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
removeItem: publicProcedure.input(queryCacheBoardInput).mutation(async ({ ctx, input }) => {
|
removeItem: internalProcedure.input(queryCacheBoardInput).mutation(async ({ ctx, input }) => {
|
||||||
await validateBoardAccessAsync(ctx, input.boardId);
|
await validateBoardAccessAsync(ctx, input.boardId);
|
||||||
const userId = ctx.session?.user.id ?? "anonymous";
|
const userId = ctx.session?.user.id ?? "anonymous";
|
||||||
await removeQueryCacheAsync(userId, input.boardId);
|
await removeQueryCacheAsync(userId, input.boardId);
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { and, eq, inArray } from "@homarr/db";
|
import { getServerSettingByKeyAsync, getServerSettingsAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries";
|
||||||
import {
|
|
||||||
getServerSettingByKeyAsync,
|
|
||||||
getServerSettingsAsync,
|
|
||||||
insertServerSettingByKeyAsync,
|
|
||||||
updateServerSettingByKeyAsync,
|
|
||||||
} from "@homarr/db/queries";
|
|
||||||
import { boards, serverSettings } from "@homarr/db/schema";
|
|
||||||
import type { ServerSettings } from "@homarr/server-settings";
|
import type { ServerSettings } from "@homarr/server-settings";
|
||||||
import { defaultServerSettingsKeys } from "@homarr/server-settings";
|
import { defaultServerSettingsKeys } from "@homarr/server-settings";
|
||||||
import { settingsInitSchema } from "@homarr/validation/settings";
|
import { settingsInitSchema } from "@homarr/validation/settings";
|
||||||
@@ -16,15 +8,6 @@ import { settingsInitSchema } from "@homarr/validation/settings";
|
|||||||
import { createTRPCRouter, onboardingProcedure, permissionRequiredProcedure, publicProcedure } from "../trpc";
|
import { createTRPCRouter, onboardingProcedure, permissionRequiredProcedure, publicProcedure } from "../trpc";
|
||||||
import { nextOnboardingStepAsync } from "./onboard/onboard-queries";
|
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({
|
export const serverSettingsRouter = createTRPCRouter({
|
||||||
getCulture: publicProcedure.query(async ({ ctx }) => {
|
getCulture: publicProcedure.query(async ({ ctx }) => {
|
||||||
return await getServerSettingByKeyAsync(ctx.db, "culture");
|
return await getServerSettingByKeyAsync(ctx.db, "culture");
|
||||||
@@ -32,67 +15,6 @@ export const serverSettingsRouter = createTRPCRouter({
|
|||||||
getAll: permissionRequiredProcedure.requiresPermission("admin").query(async ({ ctx }) => {
|
getAll: permissionRequiredProcedure.requiresPermission("admin").query(async ({ ctx }) => {
|
||||||
return await getServerSettingsAsync(ctx.db);
|
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
|
saveSettings: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(
|
.input(
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { stringify } from "superjson";
|
import SuperJSON from "superjson";
|
||||||
import { describe, expect, test, vi } from "vitest";
|
import { describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
import type { Session } from "@homarr/auth";
|
import type { Session } from "@homarr/auth";
|
||||||
import { createId } from "@homarr/common";
|
import { createId } from "@homarr/common";
|
||||||
import { boards, serverSettings } from "@homarr/db/schema";
|
import { serverSettings } from "@homarr/db/schema";
|
||||||
import { createDb } from "@homarr/db/test";
|
import { createDb } from "@homarr/db/test";
|
||||||
import { defaultServerSettings, defaultServerSettingsKeys } from "@homarr/server-settings";
|
import { defaultServerSettings, defaultServerSettingsKeys } from "@homarr/server-settings";
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ describe("getAll server settings", () => {
|
|||||||
await db.insert(serverSettings).values([
|
await db.insert(serverSettings).values([
|
||||||
{
|
{
|
||||||
settingKey: defaultServerSettingsKeys[0],
|
settingKey: defaultServerSettingsKeys[0],
|
||||||
value: stringify(defaultServerSettings.analytics),
|
value: SuperJSON.stringify(defaultServerSettings.analytics),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ describe("saveSettings", () => {
|
|||||||
await db.insert(serverSettings).values([
|
await db.insert(serverSettings).values([
|
||||||
{
|
{
|
||||||
settingKey: defaultServerSettingsKeys[0],
|
settingKey: defaultServerSettingsKeys[0],
|
||||||
value: stringify(defaultServerSettings.analytics),
|
value: SuperJSON.stringify(defaultServerSettings.analytics),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ describe("saveSettings", () => {
|
|||||||
expect(dbSettings).toStrictEqual([
|
expect(dbSettings).toStrictEqual([
|
||||||
{
|
{
|
||||||
settingKey: "analytics",
|
settingKey: "analytics",
|
||||||
value: stringify({
|
value: SuperJSON.stringify({
|
||||||
enableGeneral: true,
|
enableGeneral: true,
|
||||||
instanceId: null,
|
instanceId: null,
|
||||||
}),
|
}),
|
||||||
@@ -90,91 +90,3 @@ 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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -2,20 +2,16 @@ import { expect, test, vi } from "vitest";
|
|||||||
import { extractToolsFromProcedures } from "trpc-to-mcp";
|
import { extractToolsFromProcedures } from "trpc-to-mcp";
|
||||||
|
|
||||||
import { appRouter } from "../router/app";
|
import { appRouter } from "../router/app";
|
||||||
import { boardRouter } from "../router/board";
|
|
||||||
import { infoRouter } from "../router/info";
|
import { infoRouter } from "../router/info";
|
||||||
import { inviteRouter } from "../router/invite";
|
import { inviteRouter } from "../router/invite";
|
||||||
import { serverSettingsRouter } from "../router/serverSettings";
|
|
||||||
import { createTRPCRouter } from "../trpc";
|
import { createTRPCRouter } from "../trpc";
|
||||||
|
|
||||||
vi.mock("@homarr/auth", () => ({}));
|
vi.mock("@homarr/auth", () => ({}));
|
||||||
|
|
||||||
const mcpTestRouter = createTRPCRouter({
|
const mcpTestRouter = createTRPCRouter({
|
||||||
app: appRouter,
|
app: appRouter,
|
||||||
board: boardRouter,
|
|
||||||
info: infoRouter,
|
info: infoRouter,
|
||||||
invite: inviteRouter,
|
invite: inviteRouter,
|
||||||
serverSettings: serverSettingsRouter,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("MCP tools should be extractable from the router", () => {
|
test("MCP tools should be extractable from the router", () => {
|
||||||
@@ -32,12 +28,8 @@ test("MCP tools should contain expected procedures", () => {
|
|||||||
expect(toolNames).toContain("app_all");
|
expect(toolNames).toContain("app_all");
|
||||||
expect(toolNames).toContain("app_byId");
|
expect(toolNames).toContain("app_byId");
|
||||||
expect(toolNames).toContain("app_create");
|
expect(toolNames).toContain("app_create");
|
||||||
expect(toolNames).toContain("board_savePartialBoardSettings");
|
|
||||||
expect(toolNames).toContain("board_duplicateBoard");
|
|
||||||
expect(toolNames).toContain("info_getInfo");
|
expect(toolNames).toContain("info_getInfo");
|
||||||
expect(toolNames).toContain("invite_getAll");
|
expect(toolNames).toContain("invite_getAll");
|
||||||
expect(toolNames).toContain("serverSettings_getBoardSettings");
|
|
||||||
expect(toolNames).toContain("serverSettings_updateBoardSettings");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("MCP tools should have descriptions", () => {
|
test("MCP tools should have descriptions", () => {
|
||||||
|
|||||||
@@ -14,12 +14,3 @@ test("OpenAPI documentation should be generated", () => {
|
|||||||
// Assert
|
// Assert
|
||||||
expect(act).not.toThrow();
|
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");
|
|
||||||
});
|
|
||||||
@@ -116,6 +116,8 @@ const baseProcedure = env.DEMO_MODE ? t.procedure.use(enforceDemoModeReadOnly) :
|
|||||||
*/
|
*/
|
||||||
export const publicProcedure = baseProcedure;
|
export const publicProcedure = baseProcedure;
|
||||||
|
|
||||||
|
export const internalProcedure = t.procedure;
|
||||||
|
|
||||||
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
|
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
|
||||||
if (!ctx.session?.user) {
|
if (!ctx.session?.user) {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
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,6 +295,13 @@
|
|||||||
"when": 1780764500000,
|
"when": 1780764500000,
|
||||||
"tag": "0041_merge_custom_widget_url",
|
"tag": "0041_merge_custom_widget_url",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 42,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1783176100870,
|
||||||
|
"tag": "0042_add_widget_secret",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
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,6 +57,13 @@
|
|||||||
"when": 1780764500000,
|
"when": 1780764500000,
|
||||||
"tag": "0007_merge_custom_widget_url",
|
"tag": "0007_merge_custom_widget_url",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1783176100870,
|
||||||
|
"tag": "0008_add_widget_secret",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
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,6 +281,13 @@
|
|||||||
"when": 1780764500000,
|
"when": 1780764500000,
|
||||||
"tag": "0039_merge_custom_widget_url",
|
"tag": "0039_merge_custom_widget_url",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 40,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1783176100870,
|
||||||
|
"tag": "0040_add_widget_secret",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -60,8 +60,8 @@ export type HomarrDocumentationPath =
|
|||||||
| "/docs/tags/connections"
|
| "/docs/tags/connections"
|
||||||
| "/docs/tags/custom-widgets"
|
| "/docs/tags/custom-widgets"
|
||||||
| "/docs/tags/customization"
|
| "/docs/tags/customization"
|
||||||
| "/docs/tags/data"
|
|
||||||
| "/docs/tags/data-sources"
|
| "/docs/tags/data-sources"
|
||||||
|
| "/docs/tags/data"
|
||||||
| "/docs/tags/database"
|
| "/docs/tags/database"
|
||||||
| "/docs/tags/developer"
|
| "/docs/tags/developer"
|
||||||
| "/docs/tags/development"
|
| "/docs/tags/development"
|
||||||
@@ -99,8 +99,8 @@ export type HomarrDocumentationPath =
|
|||||||
| "/docs/tags/redis"
|
| "/docs/tags/redis"
|
||||||
| "/docs/tags/responsive"
|
| "/docs/tags/responsive"
|
||||||
| "/docs/tags/roles"
|
| "/docs/tags/roles"
|
||||||
| "/docs/tags/search"
|
|
||||||
| "/docs/tags/search-engines"
|
| "/docs/tags/search-engines"
|
||||||
|
| "/docs/tags/search"
|
||||||
| "/docs/tags/secrets"
|
| "/docs/tags/secrets"
|
||||||
| "/docs/tags/security"
|
| "/docs/tags/security"
|
||||||
| "/docs/tags/self-signed"
|
| "/docs/tags/self-signed"
|
||||||
@@ -132,6 +132,15 @@ export type HomarrDocumentationPath =
|
|||||||
| "/docs/advanced/running-as-different-user"
|
| "/docs/advanced/running-as-different-user"
|
||||||
| "/docs/advanced/single-sign-on"
|
| "/docs/advanced/single-sign-on"
|
||||||
| "/docs/advanced/styling"
|
| "/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/donate"
|
||||||
| "/docs/community/faq"
|
| "/docs/community/faq"
|
||||||
| "/docs/community/get-in-touch"
|
| "/docs/community/get-in-touch"
|
||||||
@@ -196,11 +205,9 @@ export type HomarrDocumentationPath =
|
|||||||
| "/docs/integrations/slskd"
|
| "/docs/integrations/slskd"
|
||||||
| "/docs/integrations/sonarr"
|
| "/docs/integrations/sonarr"
|
||||||
| "/docs/integrations/speedtest-tracker"
|
| "/docs/integrations/speedtest-tracker"
|
||||||
| "/docs/integrations/synology"
|
|
||||||
| "/docs/integrations/tdarr"
|
| "/docs/integrations/tdarr"
|
||||||
| "/docs/integrations/technitium-dns"
|
| "/docs/integrations/technitium-dns"
|
||||||
| "/docs/integrations/tracearr"
|
| "/docs/integrations/tracearr"
|
||||||
| "/docs/integrations/traefik"
|
|
||||||
| "/docs/integrations/transmission"
|
| "/docs/integrations/transmission"
|
||||||
| "/docs/integrations/truenas"
|
| "/docs/integrations/truenas"
|
||||||
| "/docs/integrations/umami"
|
| "/docs/integrations/umami"
|
||||||
@@ -259,11 +266,9 @@ export type HomarrDocumentationPath =
|
|||||||
| "/docs/widgets/smart-home-execute-automation"
|
| "/docs/widgets/smart-home-execute-automation"
|
||||||
| "/docs/widgets/speedtest-tracker"
|
| "/docs/widgets/speedtest-tracker"
|
||||||
| "/docs/widgets/stock-price"
|
| "/docs/widgets/stock-price"
|
||||||
| "/docs/widgets/system-disks"
|
|
||||||
| "/docs/widgets/system-resources"
|
| "/docs/widgets/system-resources"
|
||||||
| "/docs/widgets/timetable"
|
| "/docs/widgets/timetable"
|
||||||
| "/docs/widgets/tracearr"
|
| "/docs/widgets/tracearr"
|
||||||
| "/docs/widgets/traefik"
|
|
||||||
| "/docs/widgets/umami"
|
| "/docs/widgets/umami"
|
||||||
| "/docs/widgets/ups"
|
| "/docs/widgets/ups"
|
||||||
| "/docs/widgets/uptime-kuma"
|
| "/docs/widgets/uptime-kuma"
|
||||||
|
|||||||
@@ -6,12 +6,9 @@ interface Options<TData, TInput extends Record<string, unknown>> {
|
|||||||
type CacheEntry<TData> = { data: TData; timestamp: Date; expiresAt: number };
|
type CacheEntry<TData> = { data: TData; timestamp: Date; expiresAt: number };
|
||||||
|
|
||||||
const MAX_CACHE_SIZE = 1000;
|
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 DEFAULT_TTL_MS = 10_000;
|
||||||
|
|
||||||
const evictExpired = () => {
|
const evictExpired = <TData>(cache: Map<string, CacheEntry<TData>>) => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
for (const [key, entry] of cache) {
|
for (const [key, entry] of cache) {
|
||||||
if (now >= entry.expiresAt) cache.delete(key);
|
if (now >= entry.expiresAt) cache.delete(key);
|
||||||
@@ -20,41 +17,46 @@ const evictExpired = () => {
|
|||||||
|
|
||||||
export const createRequestHandler = <TData, TInput extends Record<string, unknown>>(
|
export const createRequestHandler = <TData, TInput extends Record<string, unknown>>(
|
||||||
options: Options<TData, TInput>,
|
options: Options<TData, TInput>,
|
||||||
) => ({
|
) => {
|
||||||
handler: (input: TInput) => ({
|
const cache = new Map<string, CacheEntry<TData>>();
|
||||||
async getDataAsync(): Promise<{ data: TData; timestamp: Date }> {
|
const inflight = new Map<string, Promise<CacheEntry<TData>>>();
|
||||||
const ttl = options.cacheTtlMs ?? DEFAULT_TTL_MS;
|
|
||||||
const key = JSON.stringify(input);
|
|
||||||
|
|
||||||
const cached = cache.get(key) as CacheEntry<TData> | undefined;
|
return {
|
||||||
if (cached && Date.now() < cached.expiresAt) {
|
handler: (input: TInput) => ({
|
||||||
return { data: cached.data, timestamp: cached.timestamp };
|
async getDataAsync(): Promise<{ data: TData; timestamp: Date }> {
|
||||||
}
|
const ttl = options.cacheTtlMs ?? DEFAULT_TTL_MS;
|
||||||
if (cached) cache.delete(key);
|
const key = JSON.stringify(input);
|
||||||
|
|
||||||
const existing = inflight.get(key);
|
const cached = cache.get(key);
|
||||||
if (existing) return existing as Promise<CacheEntry<TData>>;
|
if (cached && Date.now() < cached.expiresAt) {
|
||||||
|
return { data: cached.data, timestamp: cached.timestamp };
|
||||||
|
}
|
||||||
|
if (cached) cache.delete(key);
|
||||||
|
|
||||||
const promise = options
|
const existing = inflight.get(key);
|
||||||
.requestAsync(input)
|
if (existing) return existing;
|
||||||
.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;
|
|
||||||
});
|
|
||||||
|
|
||||||
inflight.set(key, promise as Promise<CacheEntry<unknown>>);
|
const promise = options
|
||||||
return promise;
|
.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;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -50,6 +50,7 @@ import {
|
|||||||
} from "@homarr/definitions";
|
} from "@homarr/definitions";
|
||||||
import { findBestIconMatch, IconPicker } from "@homarr/forms-collection";
|
import { findBestIconMatch, IconPicker } from "@homarr/forms-collection";
|
||||||
import { createModal, useModalAction } from "@homarr/modals";
|
import { createModal, useModalAction } from "@homarr/modals";
|
||||||
|
import { showErrorNotification } from "@homarr/notifications";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
import { MaskedImage } from "@homarr/ui";
|
import { MaskedImage } from "@homarr/ui";
|
||||||
|
|
||||||
@@ -269,8 +270,24 @@ const providersWithAuth: ReleaseProviderKind[] = [
|
|||||||
const ProviderTokensSection = ({ itemId, repositories }: { itemId: string; repositories: ReleasesRepository[] }) => {
|
const ProviderTokensSection = ({ itemId, repositories }: { itemId: string; repositories: ReleasesRepository[] }) => {
|
||||||
const tRepository = useScopedI18n("widget.releases.option.repositories");
|
const tRepository = useScopedI18n("widget.releases.option.repositories");
|
||||||
const { data: configuredKinds = [], refetch } = clientApi.widget.secrets.getConfiguredKinds.useQuery({ itemId });
|
const { data: configuredKinds = [], refetch } = clientApi.widget.secrets.getConfiguredKinds.useQuery({ itemId });
|
||||||
const setSecret = clientApi.widget.secrets.setSecret.useMutation({ onSuccess: () => refetch() });
|
const setSecret = clientApi.widget.secrets.setSecret.useMutation({
|
||||||
const deleteSecret = clientApi.widget.secrets.deleteSecret.useMutation({ onSuccess: () => refetch() });
|
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 authProviders = useMemo(() => {
|
const authProviders = useMemo(() => {
|
||||||
const usedProviders = repositories.map((r) => r.provider).filter((p): p is ReleaseProviderKind => p !== undefined);
|
const usedProviders = repositories.map((r) => r.provider).filter((p): p is ReleaseProviderKind => p !== undefined);
|
||||||
|
|||||||
Generated
+54
-69
@@ -142,14 +142,14 @@ catalogs:
|
|||||||
specifier: ^5.101.0
|
specifier: ^5.101.0
|
||||||
version: 5.101.0
|
version: 5.101.0
|
||||||
'@testcontainers/mysql':
|
'@testcontainers/mysql':
|
||||||
specifier: ^12.0.3
|
specifier: ^12.0.4
|
||||||
version: 12.0.3
|
version: 12.0.4
|
||||||
'@testcontainers/postgresql':
|
'@testcontainers/postgresql':
|
||||||
specifier: ^12.0.3
|
specifier: ^12.0.4
|
||||||
version: 12.0.3
|
version: 12.0.4
|
||||||
'@testcontainers/redis':
|
'@testcontainers/redis':
|
||||||
specifier: ^12.0.3
|
specifier: ^12.0.4
|
||||||
version: 12.0.3
|
version: 12.0.4
|
||||||
'@tiptap/extension-color':
|
'@tiptap/extension-color':
|
||||||
specifier: 3.27.1
|
specifier: 3.27.1
|
||||||
version: 3.27.1
|
version: 3.27.1
|
||||||
@@ -442,8 +442,8 @@ catalogs:
|
|||||||
specifier: ^1.18.0
|
specifier: ^1.18.0
|
||||||
version: 1.18.0
|
version: 1.18.0
|
||||||
posthog-js:
|
posthog-js:
|
||||||
specifier: ^1.395.0
|
specifier: ^1.396.1
|
||||||
version: 1.395.0
|
version: 1.396.3
|
||||||
posthog-node:
|
posthog-node:
|
||||||
specifier: ^4.18.0
|
specifier: ^4.18.0
|
||||||
version: 4.18.0
|
version: 4.18.0
|
||||||
@@ -496,8 +496,8 @@ catalogs:
|
|||||||
specifier: ^5.3.1
|
specifier: ^5.3.1
|
||||||
version: 5.3.1
|
version: 5.3.1
|
||||||
testcontainers:
|
testcontainers:
|
||||||
specifier: ^12.0.3
|
specifier: ^12.0.4
|
||||||
version: 12.0.3
|
version: 12.0.4
|
||||||
timezones-ical-library:
|
timezones-ical-library:
|
||||||
specifier: ^2.2.1
|
specifier: ^2.2.1
|
||||||
version: 2.2.1
|
version: 2.2.1
|
||||||
@@ -615,7 +615,7 @@ importers:
|
|||||||
version: 14.1.1(semantic-release@25.0.5(typescript@6.0.3))
|
version: 14.1.1(semantic-release@25.0.5(typescript@6.0.3))
|
||||||
'@testcontainers/redis':
|
'@testcontainers/redis':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 12.0.3
|
version: 12.0.4
|
||||||
'@turbo/gen':
|
'@turbo/gen':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 2.10.0(@types/node@24.13.2)
|
version: 2.10.0(@types/node@24.13.2)
|
||||||
@@ -651,7 +651,7 @@ importers:
|
|||||||
version: 25.0.5(typescript@6.0.3)
|
version: 25.0.5(typescript@6.0.3)
|
||||||
testcontainers:
|
testcontainers:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 12.0.3
|
version: 12.0.4
|
||||||
turbo:
|
turbo:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 2.10.0
|
version: 2.10.0
|
||||||
@@ -1012,7 +1012,7 @@ importers:
|
|||||||
version: 1.18.0(postcss@8.5.15)
|
version: 1.18.0(postcss@8.5.15)
|
||||||
posthog-js:
|
posthog-js:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 1.395.0
|
version: 1.396.3
|
||||||
prismjs:
|
prismjs:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 1.30.0
|
version: 1.30.0
|
||||||
@@ -1688,10 +1688,10 @@ importers:
|
|||||||
version: 3.3.0
|
version: 3.3.0
|
||||||
'@testcontainers/mysql':
|
'@testcontainers/mysql':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 12.0.3
|
version: 12.0.4
|
||||||
'@testcontainers/postgresql':
|
'@testcontainers/postgresql':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 12.0.3
|
version: 12.0.4
|
||||||
better-sqlite3:
|
better-sqlite3:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 12.10.0
|
version: 12.10.0
|
||||||
@@ -6400,11 +6400,11 @@ packages:
|
|||||||
'@polka/url@1.0.0-next.25':
|
'@polka/url@1.0.0-next.25':
|
||||||
resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==}
|
resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==}
|
||||||
|
|
||||||
'@posthog/core@1.38.0':
|
'@posthog/core@1.39.2':
|
||||||
resolution: {integrity: sha512-QLrJh0hMVpEJXHNyiJR9YhvIO5tzLDedw4UHdvB+ub4fXHMtHCA8H44qgl11HWR3ajpjxtJ8hs3HyOGVF3Zc1w==}
|
resolution: {integrity: sha512-G/TL5Xykl+9xCDtBwHHPRSToS/43UjzyxKOfhlTwu/hJHt3v39Q2OFu8hdum71Iu7+tuxxISGVcFMlIST7ksjA==}
|
||||||
|
|
||||||
'@posthog/types@1.391.1':
|
'@posthog/types@1.392.0':
|
||||||
resolution: {integrity: sha512-ASwd7Nf4pViqdYRYaNRyPYRVKWa1CcHUAUWR0XeQJLGdNnsWACBwe0sSieb/cHnKsRXjRwO/23KIY83lm/Ccpw==}
|
resolution: {integrity: sha512-nctNujXL3FC1v99FktaTMSugSD9ZOZekEpahUSafkU2TSvW+XGKNkQZbokuJtiWvPBK208dwMJva8UfBkChqpw==}
|
||||||
|
|
||||||
'@protobufjs/aspromise@1.1.2':
|
'@protobufjs/aspromise@1.1.2':
|
||||||
resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
|
resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
|
||||||
@@ -7332,14 +7332,14 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: ^2.7.0 || ^3.0.0
|
vue: ^2.7.0 || ^3.0.0
|
||||||
|
|
||||||
'@testcontainers/mysql@12.0.3':
|
'@testcontainers/mysql@12.0.4':
|
||||||
resolution: {integrity: sha512-xaub3gaAQKR4Q2/9d8B+81XM8LLTQHPOncCL3Ved04JUUIeTobMt3J36JXv/rvEpnrMpphlKYlmQU0aq48HeZA==}
|
resolution: {integrity: sha512-XZWh/L6EN8XQa5lsBdESDolDmyUA56bDgYh5SicLzzC2b4jho4lc0yA0GOOVX/bd0zK6VQK6hmDDJE+T3rFrdA==}
|
||||||
|
|
||||||
'@testcontainers/postgresql@12.0.3':
|
'@testcontainers/postgresql@12.0.4':
|
||||||
resolution: {integrity: sha512-y0l/ItWxNfdMU783EOZ66lMq90uuuXPqE+fo1jNSRfMbtfUq/9Ez/wluDxxaS+D/i3I0juep+jmz/WVuip8i9Q==}
|
resolution: {integrity: sha512-a/pLU6j5lpKKAlUTPwqweqMGhOSjgTSb6HBX69TOrXn32ifU37nnQDmNFTj8ddOAw+BQL9oTRkeOxVbZkqhgZA==}
|
||||||
|
|
||||||
'@testcontainers/redis@12.0.3':
|
'@testcontainers/redis@12.0.4':
|
||||||
resolution: {integrity: sha512-hN3YedOSb6quFzG1IM4j/r/M635GcaYOxzbTuiSN7676BZHrx1VPl+xll3ojk8ds6TvenrfO37lrEVJYN2URlA==}
|
resolution: {integrity: sha512-44Ov3Lcptb3KqljcCy6xIJ2VxNsN+3Kw/YpBiotZpOcI4iLgLNmcOgvCFPVo+vDoh4rCfgtNUEbIpL5Mu7oRuw==}
|
||||||
|
|
||||||
'@tiptap/core@3.27.1':
|
'@tiptap/core@3.27.1':
|
||||||
resolution: {integrity: sha512-rV6Qn4wmC6BxfF+4mu6bqGWj9vA4oXXhsrpXaJL2uhjxeHAGofjwcHof2X84VYzeyXgdlsGmqKie4TAppVXZUQ==}
|
resolution: {integrity: sha512-rV6Qn4wmC6BxfF+4mu6bqGWj9vA4oXXhsrpXaJL2uhjxeHAGofjwcHof2X84VYzeyXgdlsGmqKie4TAppVXZUQ==}
|
||||||
@@ -9673,10 +9673,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==}
|
resolution: {integrity: sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==}
|
||||||
engines: {node: '>= 8.0'}
|
engines: {node: '>= 8.0'}
|
||||||
|
|
||||||
dockerode@5.0.0:
|
|
||||||
resolution: {integrity: sha512-C52mvJ+7lcyhWNfrzVfFsbTrBfy/ezE9FGEYLpu17FUeBcCkxERk9nN7uDl/478ynDiQ4U+5DbQC2vENHkVEtQ==}
|
|
||||||
engines: {node: '>= 14.17'}
|
|
||||||
|
|
||||||
dockerode@5.0.1:
|
dockerode@5.0.1:
|
||||||
resolution: {integrity: sha512-avsq/xk4YPIrn0CgleX5bjT9Y8IT1p9PxrNQ++RBQ2WEyFfHCTDsT9kmyxz+H/axnjAwg8wJWEIuPGOUuNupiA==}
|
resolution: {integrity: sha512-avsq/xk4YPIrn0CgleX5bjT9Y8IT1p9PxrNQ++RBQ2WEyFfHCTDsT9kmyxz+H/axnjAwg8wJWEIuPGOUuNupiA==}
|
||||||
engines: {node: '>= 14.17'}
|
engines: {node: '>= 14.17'}
|
||||||
@@ -10424,9 +10420,9 @@ packages:
|
|||||||
get-own-enumerable-property-symbols@3.0.2:
|
get-own-enumerable-property-symbols@3.0.2:
|
||||||
resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==}
|
resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==}
|
||||||
|
|
||||||
get-port@7.2.0:
|
get-port@5.1.1:
|
||||||
resolution: {integrity: sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg==}
|
resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
get-proto@1.0.1:
|
get-proto@1.0.1:
|
||||||
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
||||||
@@ -13115,8 +13111,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-Ray65LYEJrMMqDtsBUBXunEVP/g4wtATvq/xz9rchUoLy/9mSkkFgUko/8DVtGxgiP3vivpFMgfb9HpCuDrBHg==}
|
resolution: {integrity: sha512-Ray65LYEJrMMqDtsBUBXunEVP/g4wtATvq/xz9rchUoLy/9mSkkFgUko/8DVtGxgiP3vivpFMgfb9HpCuDrBHg==}
|
||||||
engines: {node: '>=10.15.1'}
|
engines: {node: '>=10.15.1'}
|
||||||
|
|
||||||
posthog-js@1.395.0:
|
posthog-js@1.396.3:
|
||||||
resolution: {integrity: sha512-5iTb00CGt2eQUUiBQysQiX89RAbCN6wK2sDNzvs9zv0alaY8mJ0ZySrUD3LQ+XyLhgM5pCpacBuUwChqiYDLDw==}
|
resolution: {integrity: sha512-CL4mH3F2azsONko/zC2931RLdt6RxTsEr7x7Lyk0SDmj5hZuvBCTbg9MKViXwSRhBr3/SaGcaHS1n95WxTrsCw==}
|
||||||
|
|
||||||
posthog-node@4.18.0:
|
posthog-node@4.18.0:
|
||||||
resolution: {integrity: sha512-XROs1h+DNatgKh/AlIlCtDxWzwrKdYDb2mOs58n4yN8BkGN9ewqeQwG5ApS4/IzwCb7HPttUkOVulkYatd2PIw==}
|
resolution: {integrity: sha512-XROs1h+DNatgKh/AlIlCtDxWzwrKdYDb2mOs58n4yN8BkGN9ewqeQwG5ApS4/IzwCb7HPttUkOVulkYatd2PIw==}
|
||||||
@@ -14372,8 +14368,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==}
|
resolution: {integrity: sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
testcontainers@12.0.3:
|
testcontainers@12.0.4:
|
||||||
resolution: {integrity: sha512-6vfiL9CWIX0rWhTJitzrzHcv4Q/sUfUdBV12jMz6HZ58Lz6ZtXVecO6jljd1O3ookEWQHNICiYoYNEPpDLzuRw==}
|
resolution: {integrity: sha512-QIR/8xF1+F/26cIM+9B4yyxNTbKJxAv3hygZyhPRgZ8Q2AhlPZjDdpXRuk16V37X4bgJRI3hXFhoEICMBA7Adg==}
|
||||||
|
|
||||||
text-decoder@1.2.0:
|
text-decoder@1.2.0:
|
||||||
resolution: {integrity: sha512-n1yg1mOj9DNpk3NeZOx7T6jchTbyJS3i3cucbNN6FcdPriMZx7NsgrGpWWdWZZGxD7ES1XB+3uoqHMgOKaN+fg==}
|
resolution: {integrity: sha512-n1yg1mOj9DNpk3NeZOx7T6jchTbyJS3i3cucbNN6FcdPriMZx7NsgrGpWWdWZZGxD7ES1XB+3uoqHMgOKaN+fg==}
|
||||||
@@ -14665,8 +14661,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==}
|
resolution: {integrity: sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==}
|
||||||
engines: {node: '>=20.18.1'}
|
engines: {node: '>=20.18.1'}
|
||||||
|
|
||||||
undici@8.4.1:
|
undici@8.5.0:
|
||||||
resolution: {integrity: sha512-RNHlB4fxZK0IrkhBsxhlbx7s8kFWwr7rzzOqj5nvZugw3ig3RsB7KW3zVlV0eu8POl+rx5d1hmL7rRg0z1owow==}
|
resolution: {integrity: sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==}
|
||||||
engines: {node: '>=22.19.0'}
|
engines: {node: '>=22.19.0'}
|
||||||
|
|
||||||
unhead@2.1.15:
|
unhead@2.1.15:
|
||||||
@@ -19875,11 +19871,11 @@ snapshots:
|
|||||||
|
|
||||||
'@polka/url@1.0.0-next.25': {}
|
'@polka/url@1.0.0-next.25': {}
|
||||||
|
|
||||||
'@posthog/core@1.38.0':
|
'@posthog/core@1.39.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@posthog/types': 1.391.1
|
'@posthog/types': 1.392.0
|
||||||
|
|
||||||
'@posthog/types@1.391.1': {}
|
'@posthog/types@1.392.0': {}
|
||||||
|
|
||||||
'@protobufjs/aspromise@1.1.2': {}
|
'@protobufjs/aspromise@1.1.2': {}
|
||||||
|
|
||||||
@@ -20957,23 +20953,23 @@ snapshots:
|
|||||||
'@tanstack/virtual-core': 3.17.0
|
'@tanstack/virtual-core': 3.17.0
|
||||||
vue: 3.5.35(typescript@6.0.3)
|
vue: 3.5.35(typescript@6.0.3)
|
||||||
|
|
||||||
'@testcontainers/mysql@12.0.3':
|
'@testcontainers/mysql@12.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
testcontainers: 12.0.3
|
testcontainers: 12.0.4
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bare-buffer
|
- bare-buffer
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@testcontainers/postgresql@12.0.3':
|
'@testcontainers/postgresql@12.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
testcontainers: 12.0.3
|
testcontainers: 12.0.4
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bare-buffer
|
- bare-buffer
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@testcontainers/redis@12.0.3':
|
'@testcontainers/redis@12.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
testcontainers: 12.0.3
|
testcontainers: 12.0.4
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bare-buffer
|
- bare-buffer
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -23469,17 +23465,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
dockerode@5.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@balena/dockerignore': 1.0.2
|
'@balena/dockerignore': 1.0.2
|
||||||
@@ -24337,7 +24322,7 @@ snapshots:
|
|||||||
|
|
||||||
get-own-enumerable-property-symbols@3.0.2: {}
|
get-own-enumerable-property-symbols@3.0.2: {}
|
||||||
|
|
||||||
get-port@7.2.0: {}
|
get-port@5.1.1: {}
|
||||||
|
|
||||||
get-proto@1.0.1:
|
get-proto@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -27419,12 +27404,12 @@ snapshots:
|
|||||||
|
|
||||||
posthog-docusaurus@2.0.5: {}
|
posthog-docusaurus@2.0.5: {}
|
||||||
|
|
||||||
posthog-js@1.395.0:
|
posthog-js@1.396.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@posthog/core': 1.38.0
|
'@posthog/core': 1.39.2
|
||||||
'@posthog/types': 1.391.1
|
'@posthog/types': 1.392.0
|
||||||
core-js: 3.49.0
|
core-js: 3.49.0
|
||||||
dompurify: 3.4.10
|
dompurify: 3.4.11
|
||||||
fflate: 0.4.8
|
fflate: 0.4.8
|
||||||
preact: 10.29.2
|
preact: 10.29.2
|
||||||
query-selector-shadow-dom: 1.0.1
|
query-selector-shadow-dom: 1.0.1
|
||||||
@@ -29049,7 +29034,7 @@ snapshots:
|
|||||||
commander: 2.20.3
|
commander: 2.20.3
|
||||||
source-map-support: 0.5.21
|
source-map-support: 0.5.21
|
||||||
|
|
||||||
testcontainers@12.0.3:
|
testcontainers@12.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@balena/dockerignore': 1.0.2
|
'@balena/dockerignore': 1.0.2
|
||||||
'@types/dockerode': 4.0.1
|
'@types/dockerode': 4.0.1
|
||||||
@@ -29058,14 +29043,14 @@ snapshots:
|
|||||||
byline: 5.0.0
|
byline: 5.0.0
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
docker-compose: 1.4.2
|
docker-compose: 1.4.2
|
||||||
dockerode: 5.0.0
|
dockerode: 5.0.1
|
||||||
get-port: 7.2.0
|
get-port: 5.1.1
|
||||||
proper-lockfile: 4.1.2
|
proper-lockfile: 4.1.2
|
||||||
properties-reader: 3.0.1
|
properties-reader: 3.0.1
|
||||||
ssh-remote-port-forward: 1.0.4
|
ssh-remote-port-forward: 1.0.4
|
||||||
tar-fs: 3.1.2
|
tar-fs: 3.1.3
|
||||||
tmp: 0.2.7
|
tmp: 0.2.7
|
||||||
undici: 8.4.1
|
undici: 8.5.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bare-buffer
|
- bare-buffer
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -29324,7 +29309,7 @@ snapshots:
|
|||||||
|
|
||||||
undici@7.27.2: {}
|
undici@7.27.2: {}
|
||||||
|
|
||||||
undici@8.4.1: {}
|
undici@8.5.0: {}
|
||||||
|
|
||||||
unhead@2.1.15:
|
unhead@2.1.15:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
+5
-5
@@ -50,9 +50,9 @@ catalog:
|
|||||||
"@tanstack/react-query-devtools": ^5.101.0
|
"@tanstack/react-query-devtools": ^5.101.0
|
||||||
"@tanstack/react-query-next-experimental": ^5.101.0
|
"@tanstack/react-query-next-experimental": ^5.101.0
|
||||||
"@tanstack/react-query-persist-client": ^5.101.0
|
"@tanstack/react-query-persist-client": ^5.101.0
|
||||||
"@testcontainers/mysql": ^12.0.3
|
"@testcontainers/mysql": ^12.0.4
|
||||||
"@testcontainers/postgresql": ^12.0.3
|
"@testcontainers/postgresql": ^12.0.4
|
||||||
"@testcontainers/redis": ^12.0.3
|
"@testcontainers/redis": ^12.0.4
|
||||||
"@tiptap/extension-color": 3.27.1
|
"@tiptap/extension-color": 3.27.1
|
||||||
"@tiptap/extension-highlight": 3.27.1
|
"@tiptap/extension-highlight": 3.27.1
|
||||||
"@tiptap/extension-image": 3.27.1
|
"@tiptap/extension-image": 3.27.1
|
||||||
@@ -93,7 +93,7 @@ catalog:
|
|||||||
"@types/video.js": ^7.3.58
|
"@types/video.js": ^7.3.58
|
||||||
"@types/ws": ^8.18.1
|
"@types/ws": ^8.18.1
|
||||||
"@types/xml2js": ^0.4.14
|
"@types/xml2js": ^0.4.14
|
||||||
posthog-js: ^1.395.0
|
posthog-js: ^1.396.1
|
||||||
posthog-node: ^4.18.0
|
posthog-node: ^4.18.0
|
||||||
"@vitejs/plugin-react": ^6.0.3
|
"@vitejs/plugin-react": ^6.0.3
|
||||||
"@vitest/coverage-v8": ^4.1.9
|
"@vitest/coverage-v8": ^4.1.9
|
||||||
@@ -170,7 +170,7 @@ catalog:
|
|||||||
semver-parser: ^4.2.1
|
semver-parser: ^4.2.1
|
||||||
superjson: 2.2.6
|
superjson: 2.2.6
|
||||||
svgson: ^5.3.1
|
svgson: ^5.3.1
|
||||||
testcontainers: ^12.0.3
|
testcontainers: ^12.0.4
|
||||||
timezones-ical-library: ^2.2.1
|
timezones-ical-library: ^2.2.1
|
||||||
trpc-to-mcp: ^1.3.2
|
trpc-to-mcp: ^1.3.2
|
||||||
trpc-to-openapi: ^3.3.0
|
trpc-to-openapi: ^3.3.0
|
||||||
|
|||||||
Reference in new issue
Block a user