From 05292ae1f503a8acb3e4296dd79db43b19cd5ea8 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Mon, 3 Aug 2026 15:24:30 +0200 Subject: [PATCH 1/6] fix: map legacy media server options --- packages/old-import/src/widgets/options.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/old-import/src/widgets/options.ts b/packages/old-import/src/widgets/options.ts index 0cf84b493..b2ed298a7 100644 --- a/packages/old-import/src/widgets/options.ts +++ b/packages/old-import/src/widgets/options.ts @@ -148,6 +148,8 @@ const optionMapping: OptionMapping = { }, mediaServer: { showOnlyPlaying: () => undefined, + showBitrate: () => undefined, + showLocation: () => undefined, }, indexerManager: { openIndexerSiteInNewTab: (oldOptions) => oldOptions.openIndexerSiteInNewTab, From f1b7a5146ab7da4f5138c43bbc37a00d52d95b7c Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 3 Aug 2026 16:47:40 +0200 Subject: [PATCH 2/6] fix: allow [homarr_base] template variables in app href validation (#6508) --- packages/definitions/src/docs/homarr-docs-sitemap.ts | 1 + packages/validation/src/app.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/packages/definitions/src/docs/homarr-docs-sitemap.ts b/packages/definitions/src/docs/homarr-docs-sitemap.ts index 22bc5f75b..7185a2fdd 100644 --- a/packages/definitions/src/docs/homarr-docs-sitemap.ts +++ b/packages/definitions/src/docs/homarr-docs-sitemap.ts @@ -119,6 +119,7 @@ export type HomarrDocumentationPath = | "/docs/tags/variables" | "/docs/tags/widgets" | "/docs/advanced/command-line" + | "/docs/advanced/command-line/development" | "/docs/advanced/command-line/fix-usernames" | "/docs/advanced/command-line/integrations" | "/docs/advanced/command-line/password-recovery" diff --git a/packages/validation/src/app.ts b/packages/validation/src/app.ts index 21a4335be..e3b94218b 100644 --- a/packages/validation/src/app.ts +++ b/packages/validation/src/app.ts @@ -5,6 +5,10 @@ export const appHrefSchema = z .trim() .url() .regex(/^(?!javascript)[a-zA-Z]*:\/\//i) // javascript: is not allowed, i for case insensitive (so Javascript: is also not allowed) + .or(z.string().trim().includes("[homarr_base]")) + .or(z.string().trim().includes("[homarr_hostname]")) + .or(z.string().trim().includes("[homarr_domain]")) + .or(z.string().trim().includes("[homarr_protocol]")) .or(z.literal("")) .transform((value) => (value.length === 0 ? null : value)) .nullable(); From 2f6b96e5ba92d950d5280d9ec3566cbc30c0c2c1 Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 3 Aug 2026 17:04:35 +0200 Subject: [PATCH 3/6] feat: restrict integrations management to integration-full-all permission (#6509) --- .../app/[locale]/manage/integrations/edit/[id]/page.tsx | 7 +++++++ .../src/app/[locale]/manage/integrations/new/page.tsx | 5 ++++- apps/nextjs/src/app/[locale]/manage/integrations/page.tsx | 5 ++++- apps/nextjs/src/app/[locale]/manage/layout.tsx | 2 +- packages/api/src/router/home.ts | 2 +- packages/api/src/router/integration/integration-router.ts | 6 ++++-- packages/spotlight/src/modes/command/global-group.tsx | 4 +++- packages/spotlight/src/modes/page/pages-search-group.tsx | 2 +- 8 files changed, 25 insertions(+), 8 deletions(-) diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/edit/[id]/page.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/edit/[id]/page.tsx index 8cd8bf01b..ed2da1aab 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/edit/[id]/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/integrations/edit/[id]/page.tsx @@ -1,5 +1,7 @@ +import { notFound } from "next/navigation"; import { Container, Fieldset, Group, Stack, Title } from "@mantine/core"; +import { auth } from "@homarr/auth/next"; import { api } from "@homarr/api/server"; import { getIntegrationName } from "@homarr/definitions"; import { getI18n, getScopedI18n } from "@homarr/translation/server"; @@ -16,6 +18,11 @@ interface EditIntegrationPageProps { export default async function EditIntegrationPage(props: EditIntegrationPageProps) { const params = await props.params; + const session = await auth(); + + if (!session?.user.permissions.includes("integration-full-all")) { + notFound(); + } const editT = await getScopedI18n("integration.page.edit"); const t = await getI18n(); const integration = await api.integration.byId({ id: params.id }).catch(catchTrpcNotFound); diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/new/page.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/new/page.tsx index 3194134c3..d6f1cce98 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/new/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/integrations/new/page.tsx @@ -23,7 +23,10 @@ interface NewIntegrationPageProps { export default async function IntegrationsNewPage(props: NewIntegrationPageProps) { const searchParams = await props.searchParams; const session = await auth(); - if (!session?.user.permissions.includes("integration-create")) { + if ( + !session?.user.permissions.includes("integration-full-all") || + !session?.user.permissions.includes("integration-create") + ) { notFound(); } diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/page.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/page.tsx index 0eeb25c1a..692329896 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/integrations/page.tsx @@ -1,5 +1,5 @@ import { Fragment } from "react"; -import { redirect } from "next/navigation"; +import { notFound, redirect } from "next/navigation"; import { AccordionControl, AccordionItem, @@ -50,6 +50,9 @@ export default async function IntegrationsPage(props: IntegrationsPageProps) { if (!session) { redirect("/auth/login"); } + if (!session.user.permissions.includes("integration-full-all")) { + notFound(); + } const integrations = await api.integration.all(); const t = await getScopedI18n("integration"); diff --git a/apps/nextjs/src/app/[locale]/manage/layout.tsx b/apps/nextjs/src/app/[locale]/manage/layout.tsx index 1cd0d3d2e..ba2440c25 100644 --- a/apps/nextjs/src/app/[locale]/manage/layout.tsx +++ b/apps/nextjs/src/app/[locale]/manage/layout.tsx @@ -71,7 +71,7 @@ export default async function ManageLayout({ children }: PropsWithChildren) { icon: IconAffiliateFilled, href: "/manage/integrations", label: t("items.integrations"), - hidden: !session?.user.permissions.includes("integration-create"), + hidden: !session?.user.permissions.includes("integration-full-all"), "data-onboarding-tour-id": "manage-integrations", }, { diff --git a/packages/api/src/router/home.ts b/packages/api/src/router/home.ts index 4b63f9326..a9cee2c21 100644 --- a/packages/api/src/router/home.ts +++ b/packages/api/src/router/home.ts @@ -92,7 +92,7 @@ export const homeRouter = createTRPCRouter({ }); } - if (ctx.session?.user.permissions.includes("integration-create")) { + if (ctx.session?.user.permissions.includes("integration-full-all")) { statistics.push({ titleKey: "integration", subtitleKey: "resources", diff --git a/packages/api/src/router/integration/integration-router.ts b/packages/api/src/router/integration/integration-router.ts index af237bb31..2b40dc2a8 100644 --- a/packages/api/src/router/integration/integration-router.ts +++ b/packages/api/src/router/integration/integration-router.ts @@ -62,12 +62,13 @@ export const integrationRouter = createTRPCRouter({ requiredSecrets: def.secretKinds, })); }), - all: protectedProcedure + all: permissionRequiredProcedure + .requiresPermission("integration-full-all") .meta({ mcp: { enabled: true, description: - "List all configured integrations (connections to services like Sonarr, Radarr, Plex, etc.). Returns each integration's id, name, kind, url, and permissions. Use the 'id' field as 'integrationId' in other tools. Check permissions.hasUseAccess before reading data and permissions.hasInteractAccess before performing actions — false means the API key owner lacks that permission level for this integration, not an error", + "List all configured integrations (connections to services like Sonarr, Radarr, Plex, etc.). Requires integration-full-all permission. Returns each integration's id, name, kind, url, and permissions. Use the 'id' field as 'integrationId' in other tools. Check permissions.hasUseAccess before reading data and permissions.hasInteractAccess before performing actions — false means the API key owner lacks that permission level for this integration, not an error", }, }) .query(async ({ ctx }) => { @@ -296,6 +297,7 @@ export const integrationRouter = createTRPCRouter({ }; }), create: permissionRequiredProcedure + .requiresPermission("integration-full-all") .requiresPermission("integration-create") .meta({ mcp: { diff --git a/packages/spotlight/src/modes/command/global-group.tsx b/packages/spotlight/src/modes/command/global-group.tsx index fd277b55e..fa8f53c5f 100644 --- a/packages/spotlight/src/modes/command/global-group.tsx +++ b/packages/spotlight/src/modes/command/global-group.tsx @@ -95,7 +95,9 @@ export const globalCommandGroup = createGroup({ icon: IconPlug, name: tOption("newIntegration.label"), useInteraction: interaction.children(newIntegrationChildrenOptions), - hidden: !session?.user.permissions.includes("integration-create"), + hidden: + !session?.user.permissions.includes("integration-full-all") || + !session?.user.permissions.includes("integration-create"), }, { commandKey: "newUser", diff --git a/packages/spotlight/src/modes/page/pages-search-group.tsx b/packages/spotlight/src/modes/page/pages-search-group.tsx index 66fe1ce4f..1b3b0ee68 100644 --- a/packages/spotlight/src/modes/page/pages-search-group.tsx +++ b/packages/spotlight/src/modes/page/pages-search-group.tsx @@ -82,7 +82,7 @@ export const pagesSearchGroup = createGroup<{ icon: IconPlug, path: "/manage/integrations", name: t("manageIntegration.label"), - hidden: !session, + hidden: !session?.user.permissions.includes("integration-full-all"), }, { icon: IconSearch, From 76ddfceb130d88fa1005c5e14961851b880b898f Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 3 Aug 2026 17:07:46 +0200 Subject: [PATCH 4/6] fix: restore auto-refresh for docker and notifications widgets (#6511) --- packages/widgets/src/docker/index.ts | 1 + packages/widgets/src/notifications/index.ts | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/widgets/src/docker/index.ts b/packages/widgets/src/docker/index.ts index 03c6e990d..bc229fed5 100644 --- a/packages/widgets/src/docker/index.ts +++ b/packages/widgets/src/docker/index.ts @@ -26,6 +26,7 @@ const columnTranslationKeyMap = { export const { definition, componentLoader } = createWidgetDefinition("dockerContainers", { icon: IconBrandDocker, queryKey: [["docker", "getContainers"]], + refetchInterval: 30, createOptions() { return optionsBuilder.from( (factory) => ({ diff --git a/packages/widgets/src/notifications/index.ts b/packages/widgets/src/notifications/index.ts index 17fd13393..553ef5d9f 100644 --- a/packages/widgets/src/notifications/index.ts +++ b/packages/widgets/src/notifications/index.ts @@ -7,7 +7,6 @@ import { optionsBuilder } from "../options"; export const { componentLoader, definition } = createWidgetDefinition("notifications", { icon: IconMessage, - refetchInterval: null, createOptions() { return optionsBuilder.from((factory) => ({ hideLogos: factory.switch({ defaultValue: false }), From b4fba47a23ff4ada4670d4b4c69be6922dc8c2c4 Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 3 Aug 2026 17:10:35 +0200 Subject: [PATCH 5/6] fix(releases): add success flag to successful response for correct error detection (#6512) --- packages/api/src/router/widgets/releases.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/api/src/router/widgets/releases.ts b/packages/api/src/router/widgets/releases.ts index d234a75c1..7480de536 100644 --- a/packages/api/src/router/widgets/releases.ts +++ b/packages/api/src/router/widgets/releases.ts @@ -104,6 +104,7 @@ export const releasesRouter = createTRPCRouter({ id: repositoryId, provider: repository.provider, timestamp: response.timestamp, + success: true as const, ...response.data, }; } catch (error) { From 778533a62e7b0316479824c5e6b6fa23a085c3e6 Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 3 Aug 2026 17:37:53 +0200 Subject: [PATCH 6/6] fix: revert regressions from recent integration/releases/app-href changes (#6514) --- .../app/[locale]/manage/integrations/edit/[id]/page.tsx | 7 ------- .../src/app/[locale]/manage/integrations/new/page.tsx | 5 +---- apps/nextjs/src/app/[locale]/manage/integrations/page.tsx | 5 +---- apps/nextjs/src/app/[locale]/manage/layout.tsx | 2 +- packages/api/src/router/home.ts | 2 +- packages/api/src/router/integration/integration-router.ts | 6 ++---- packages/api/src/router/widgets/releases.ts | 1 - packages/definitions/src/docs/homarr-docs-sitemap.ts | 1 - packages/spotlight/src/modes/command/global-group.tsx | 4 +--- packages/spotlight/src/modes/page/pages-search-group.tsx | 2 +- packages/validation/src/app.ts | 4 ---- 11 files changed, 8 insertions(+), 31 deletions(-) diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/edit/[id]/page.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/edit/[id]/page.tsx index ed2da1aab..8cd8bf01b 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/edit/[id]/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/integrations/edit/[id]/page.tsx @@ -1,7 +1,5 @@ -import { notFound } from "next/navigation"; import { Container, Fieldset, Group, Stack, Title } from "@mantine/core"; -import { auth } from "@homarr/auth/next"; import { api } from "@homarr/api/server"; import { getIntegrationName } from "@homarr/definitions"; import { getI18n, getScopedI18n } from "@homarr/translation/server"; @@ -18,11 +16,6 @@ interface EditIntegrationPageProps { export default async function EditIntegrationPage(props: EditIntegrationPageProps) { const params = await props.params; - const session = await auth(); - - if (!session?.user.permissions.includes("integration-full-all")) { - notFound(); - } const editT = await getScopedI18n("integration.page.edit"); const t = await getI18n(); const integration = await api.integration.byId({ id: params.id }).catch(catchTrpcNotFound); diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/new/page.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/new/page.tsx index d6f1cce98..3194134c3 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/new/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/integrations/new/page.tsx @@ -23,10 +23,7 @@ interface NewIntegrationPageProps { export default async function IntegrationsNewPage(props: NewIntegrationPageProps) { const searchParams = await props.searchParams; const session = await auth(); - if ( - !session?.user.permissions.includes("integration-full-all") || - !session?.user.permissions.includes("integration-create") - ) { + if (!session?.user.permissions.includes("integration-create")) { notFound(); } diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/page.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/page.tsx index 692329896..0eeb25c1a 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/integrations/page.tsx @@ -1,5 +1,5 @@ import { Fragment } from "react"; -import { notFound, redirect } from "next/navigation"; +import { redirect } from "next/navigation"; import { AccordionControl, AccordionItem, @@ -50,9 +50,6 @@ export default async function IntegrationsPage(props: IntegrationsPageProps) { if (!session) { redirect("/auth/login"); } - if (!session.user.permissions.includes("integration-full-all")) { - notFound(); - } const integrations = await api.integration.all(); const t = await getScopedI18n("integration"); diff --git a/apps/nextjs/src/app/[locale]/manage/layout.tsx b/apps/nextjs/src/app/[locale]/manage/layout.tsx index ba2440c25..1cd0d3d2e 100644 --- a/apps/nextjs/src/app/[locale]/manage/layout.tsx +++ b/apps/nextjs/src/app/[locale]/manage/layout.tsx @@ -71,7 +71,7 @@ export default async function ManageLayout({ children }: PropsWithChildren) { icon: IconAffiliateFilled, href: "/manage/integrations", label: t("items.integrations"), - hidden: !session?.user.permissions.includes("integration-full-all"), + hidden: !session?.user.permissions.includes("integration-create"), "data-onboarding-tour-id": "manage-integrations", }, { diff --git a/packages/api/src/router/home.ts b/packages/api/src/router/home.ts index a9cee2c21..4b63f9326 100644 --- a/packages/api/src/router/home.ts +++ b/packages/api/src/router/home.ts @@ -92,7 +92,7 @@ export const homeRouter = createTRPCRouter({ }); } - if (ctx.session?.user.permissions.includes("integration-full-all")) { + if (ctx.session?.user.permissions.includes("integration-create")) { statistics.push({ titleKey: "integration", subtitleKey: "resources", diff --git a/packages/api/src/router/integration/integration-router.ts b/packages/api/src/router/integration/integration-router.ts index 2b40dc2a8..af237bb31 100644 --- a/packages/api/src/router/integration/integration-router.ts +++ b/packages/api/src/router/integration/integration-router.ts @@ -62,13 +62,12 @@ export const integrationRouter = createTRPCRouter({ requiredSecrets: def.secretKinds, })); }), - all: permissionRequiredProcedure - .requiresPermission("integration-full-all") + all: protectedProcedure .meta({ mcp: { enabled: true, description: - "List all configured integrations (connections to services like Sonarr, Radarr, Plex, etc.). Requires integration-full-all permission. Returns each integration's id, name, kind, url, and permissions. Use the 'id' field as 'integrationId' in other tools. Check permissions.hasUseAccess before reading data and permissions.hasInteractAccess before performing actions — false means the API key owner lacks that permission level for this integration, not an error", + "List all configured integrations (connections to services like Sonarr, Radarr, Plex, etc.). Returns each integration's id, name, kind, url, and permissions. Use the 'id' field as 'integrationId' in other tools. Check permissions.hasUseAccess before reading data and permissions.hasInteractAccess before performing actions — false means the API key owner lacks that permission level for this integration, not an error", }, }) .query(async ({ ctx }) => { @@ -297,7 +296,6 @@ export const integrationRouter = createTRPCRouter({ }; }), create: permissionRequiredProcedure - .requiresPermission("integration-full-all") .requiresPermission("integration-create") .meta({ mcp: { diff --git a/packages/api/src/router/widgets/releases.ts b/packages/api/src/router/widgets/releases.ts index 7480de536..d234a75c1 100644 --- a/packages/api/src/router/widgets/releases.ts +++ b/packages/api/src/router/widgets/releases.ts @@ -104,7 +104,6 @@ export const releasesRouter = createTRPCRouter({ id: repositoryId, provider: repository.provider, timestamp: response.timestamp, - success: true as const, ...response.data, }; } catch (error) { diff --git a/packages/definitions/src/docs/homarr-docs-sitemap.ts b/packages/definitions/src/docs/homarr-docs-sitemap.ts index 7185a2fdd..22bc5f75b 100644 --- a/packages/definitions/src/docs/homarr-docs-sitemap.ts +++ b/packages/definitions/src/docs/homarr-docs-sitemap.ts @@ -119,7 +119,6 @@ export type HomarrDocumentationPath = | "/docs/tags/variables" | "/docs/tags/widgets" | "/docs/advanced/command-line" - | "/docs/advanced/command-line/development" | "/docs/advanced/command-line/fix-usernames" | "/docs/advanced/command-line/integrations" | "/docs/advanced/command-line/password-recovery" diff --git a/packages/spotlight/src/modes/command/global-group.tsx b/packages/spotlight/src/modes/command/global-group.tsx index fa8f53c5f..fd277b55e 100644 --- a/packages/spotlight/src/modes/command/global-group.tsx +++ b/packages/spotlight/src/modes/command/global-group.tsx @@ -95,9 +95,7 @@ export const globalCommandGroup = createGroup({ icon: IconPlug, name: tOption("newIntegration.label"), useInteraction: interaction.children(newIntegrationChildrenOptions), - hidden: - !session?.user.permissions.includes("integration-full-all") || - !session?.user.permissions.includes("integration-create"), + hidden: !session?.user.permissions.includes("integration-create"), }, { commandKey: "newUser", diff --git a/packages/spotlight/src/modes/page/pages-search-group.tsx b/packages/spotlight/src/modes/page/pages-search-group.tsx index 1b3b0ee68..66fe1ce4f 100644 --- a/packages/spotlight/src/modes/page/pages-search-group.tsx +++ b/packages/spotlight/src/modes/page/pages-search-group.tsx @@ -82,7 +82,7 @@ export const pagesSearchGroup = createGroup<{ icon: IconPlug, path: "/manage/integrations", name: t("manageIntegration.label"), - hidden: !session?.user.permissions.includes("integration-full-all"), + hidden: !session, }, { icon: IconSearch, diff --git a/packages/validation/src/app.ts b/packages/validation/src/app.ts index e3b94218b..21a4335be 100644 --- a/packages/validation/src/app.ts +++ b/packages/validation/src/app.ts @@ -5,10 +5,6 @@ export const appHrefSchema = z .trim() .url() .regex(/^(?!javascript)[a-zA-Z]*:\/\//i) // javascript: is not allowed, i for case insensitive (so Javascript: is also not allowed) - .or(z.string().trim().includes("[homarr_base]")) - .or(z.string().trim().includes("[homarr_hostname]")) - .or(z.string().trim().includes("[homarr_domain]")) - .or(z.string().trim().includes("[homarr_protocol]")) .or(z.literal("")) .transform((value) => (value.length === 0 ? null : value)) .nullable();