feat: add assistant board styling tools

This commit is contained in:
ajnart committed 2026-08-01 10:39:41 +02:00
1 parent a5fa6d3afe
commit 5458e7d647
21 files changed
+811 -18

No files matched your search

+52
View File
@@ -52,6 +52,7 @@ import {
boardSavePartialSettingsSchema,
boardSavePermissionsSchema,
boardSaveSchema,
boardSettingsSchema,
boardSummarySchema,
} from "@homarr/validation/board";
import { byIdSchema } from "@homarr/validation/common";
@@ -682,6 +683,57 @@ export const boardRouter = createTRPCRouter({
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
}),
getBoardSettings: protectedProcedure
.meta({
mcp: {
enabled: true,
description:
"Read the editable visual and behavior settings for one board, including its current custom CSS. Requires modify permission. REQUIRED: id (board ID). Call this before proposing board settings or custom CSS changes",
},
})
.input(z.object({ id: z.string() }))
.output(boardSettingsSchema)
.query(async ({ input, ctx }) => {
const boardWhere = eq(boards.id, input.id);
await throwIfActionForbiddenAsync(ctx, boardWhere, "modify");
const board = await ctx.db.query.boards.findFirst({
columns: {
id: true,
name: true,
pageTitle: true,
metaTitle: true,
logoImageUrl: true,
faviconImageUrl: true,
backgroundImageUrl: true,
backgroundImageAttachment: true,
backgroundImageRepeat: true,
backgroundImageSize: true,
primaryColor: true,
secondaryColor: true,
opacity: true,
customCss: true,
iconColor: true,
itemRadius: true,
disableStatus: true,
},
where: boardWhere,
});
if (!board) {
throw new TRPCError({ code: "NOT_FOUND", message: "Board not found" });
}
return {
...board,
pageTitle: board.pageTitle ?? "",
metaTitle: board.metaTitle ?? "",
logoImageUrl: board.logoImageUrl ?? "",
faviconImageUrl: board.faviconImageUrl ?? "",
backgroundImageUrl: board.backgroundImageUrl ?? "",
customCss: board.customCss ?? "",
iconColor: board.iconColor ?? "",
};
}),
saveLayouts: protectedProcedure.input(boardSaveLayoutsSchema).mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
@@ -586,6 +586,28 @@ describe("getBoardByName should return board by name", () => {
});
});
describe("getBoardSettings should return editable board settings", () => {
test("returns current custom CSS and requires modify permission", async () => {
const spy = vi.spyOn(boardAccess, "throwIfActionForbiddenAsync");
const db = createDb();
const caller = boardRouter.createCaller({ db, deviceType: undefined, session: defaultSession });
const { boardId } = await createFullBoardAsync(db, "default");
const customCss = ".dashboard-card { border-radius: 0.5rem; }";
await db.update(boards).set({ customCss, primaryColor: "#123456", pageTitle: null }).where(eq(boards.id, boardId));
const result = await caller.getBoardSettings({ id: boardId });
expect(result).toMatchObject({
id: boardId,
name: "default",
customCss,
primaryColor: "#123456",
pageTitle: null,
});
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "modify");
});
});
describe("savePartialBoardSettings should save general settings", () => {
it("should save general settings", async () => {
// Arrange
+1
View File
@@ -54,6 +54,7 @@ test("MCP tools should contain expected procedures", () => {
expect(toolNames).toContain("app_byId");
expect(toolNames).toContain("app_create");
expect(toolNames).toContain("board_savePartialBoardSettings");
expect(toolNames).toContain("board_getBoardSettings");
expect(toolNames).toContain("board_duplicateBoard");
expect(toolNames).toContain("icon_findIcons");
expect(toolNames).toContain("info_getInfo");