From 778533a62e7b0316479824c5e6b6fa23a085c3e6 Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 3 Aug 2026 17:37:53 +0200 Subject: [PATCH 1/4] 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(); From 55214ccbc59143b2606c2fc61e82deaa6ad4d985 Mon Sep 17 00:00:00 2001 From: Thomas Date: Thu, 6 Aug 2026 21:42:46 +0200 Subject: [PATCH 2/4] fix(integrations): throw on total item-info failure to preserve stale-cache fallback (#6522) --- packages/definitions/src/docs/homarr-docs-sitemap.ts | 1 + .../integrations/src/overseerr/overseerr-integration.ts | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) 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/integrations/src/overseerr/overseerr-integration.ts b/packages/integrations/src/overseerr/overseerr-integration.ts index 9de482008..10aa73114 100644 --- a/packages/integrations/src/overseerr/overseerr-integration.ts +++ b/packages/integrations/src/overseerr/overseerr-integration.ts @@ -185,9 +185,15 @@ export class OverseerrIntegration }), ); - return settled + const fulfilled = settled .filter((result): result is PromiseFulfilledResult => result.status === "fulfilled") .map((result) => result.value); + + if (fulfilled.length === 0) { + throw new Error("Failed to resolve any media request information"); + } + + return fulfilled; } protected mapRequestStatus(status: UpstreamMediaRequestStatus): MediaRequestStatus { From 637f03e1077c664c7fc5472aab2e3cce02e2df44 Mon Sep 17 00:00:00 2001 From: Thomas Date: Thu, 6 Aug 2026 21:47:04 +0200 Subject: [PATCH 3/4] docs(settings): update analytics docs to PostHog (#6525) --- apps/docs/docs/management/settings/index.mdx | 134 +++++++++++-------- 1 file changed, 81 insertions(+), 53 deletions(-) diff --git a/apps/docs/docs/management/settings/index.mdx b/apps/docs/docs/management/settings/index.mdx index d7705e61a..016d56d10 100644 --- a/apps/docs/docs/management/settings/index.mdx +++ b/apps/docs/docs/management/settings/index.mdx @@ -9,97 +9,125 @@ tags: --- # Settings -The settings page has a few options that will be applied to the entire Homarr application. -They may require a full page reload for changes to apply. + +The settings page applies options to the whole Homarr application. +Some changes need a full page reload before they take effect. ## Analytics -By default, Homarr is collecting anonymous analytics to identify issues and prioritize bugfixes and improvements. -The data is never shared with any third-parties and is fully GDPR & CCPA compliant. -The software being used is [Umami Analytics](https://umami.is/) which is fully open source. -The main server software can be found [here](https://github.com/umami-software/umami) and [this node library](https://github.com/umami-software/node) to perform server side calls to the API. -A full list of the collected data by default can be found here: https://umami.is/docs/metric-definitions. +Homarr uses [PostHog](https://posthog.com/) to collect anonymous usage data. +We collect this data to see what features people ACTUALLY use. +We use this data to decide where we spend our development time. -We understand that you might not want Homarr to communicate with our services. -By unchecking the ``Send anonymous analytics`` checkbox and reloading the full page, all communication by Umami will be disabled and no data will be collected. -If you are paranoid, you can permanently block ``umami.homarr.dev`` from your DNS / firewall. +We do not sell the data. We do not share the data with third parties. +The data does not contain personal information and does not identify you or your installation. -Additionally, you opt in to more data collection to help us provide more useful data. +Homarr sends the data to our own PostHog server at `https://hog.homarr.dev`. +The server sends the data once a week and when the server starts. -### Integration data -Enabling this optional data will send the types of integrations (e.g. SABnzbd, Sonarr, ...) and how many you have configured of them. -This will not include any URLs, names, secrets, errors and others. This is disabled by default. +### What we collect -### Widget data -Enabling this optional data will send the types of widgets (e.g. downloads, media streams, iframe, ...) and how many you have configured of them. -This will not include any URLs, names, secrets, errors and others. This is disabled by default. +Homarr collects technical data about your installation: -### Users data -Enabling this optional data will send the count of users and whether you activated SSO. -This will not include any usernames, passwords, emails, URLs, authentication flows or client IDs. -This is disabled by default. +- Your Homarr version +- Your database type (SQLite, MySQL, or PostgreSQL) +- Whether Docker or Kubernetes is enabled +- Your enabled authentication providers +- Your operating system and CPU architecture +- Your default language +- The number of boards, apps, sections, layouts, users, groups, search engines, icon repositories, cron jobs, API keys, invites, sessions, and accounts +- The integration types you use and how many you have of each (for example, Sonarr or SABnzbd) +- The widget types you use and how many you have of each + +Homarr does NOT collect: + +- URLs +- Names +- Usernames, emails, or passwords +- Integration secrets or API keys +- Error messages or logs +- Any data that can identify you + +Each installation gets a random ID. We use this ID only to count unique installations. We cannot use this ID to identify you. + +Homarr does not use cookies for analytics. Homarr does not track page views automatically. Homarr only sends the data listed above. + +### Disable analytics + +Analytics are enabled by default. To disable analytics: + +1. Open the Settings page. +2. Uncheck the "Help us improve Homarr" checkbox. +3. Reload the full page. + +When analytics are disabled, Homarr does not send any data to PostHog. + +You can also set the `NO_EXTERNAL_CONNECTION` environment variable to `true`. This stops all external requests, including analytics. See the [environment variables](/docs/advanced/environment-variables) page for more information. + +You can block `hog.homarr.dev` in your DNS or firewall. Homarr cannot send data when this address is blocked. ## Search Engine Optimization (SEO) -Big companies [like Google, Microsoft, Baidu and Yandex](https://de.wikipedia.org/wiki/Web-Index) will use [web crawlers](https://de.wikipedia.org/wiki/Webcrawler) to index the Internet for new websites & pages. -This is desirable in many cases (for example when selling products in an online shop) but can lead to unwanted traffic and attention to your personal dashboard. -Homarr is requesting the crawlers to not index Homarr instances by default for this reason. -If you use Homarr as a public website and want it to be searchable on these search engines, you can disable these settings. -Please note that Homarr doesn't have control over what the crawlers do; it is a mere request to them to not perform these actions but will not guarantee that your website doesn't land on some index. + +Search engines use web crawlers to index websites. This can cause unwanted traffic to your dashboard. +Homarr asks crawlers to not index your instance by default. Use these settings to change this request. + +Homarr cannot control what the crawlers do. A crawler can ignore the request. Your website can still appear in an index. ### No index -This will ask crawlers to not index your Homarr instance on any indexes. -It is up to the crawler to properly respect this request. -By default, this setting is enabled. Disabling it will allow indexing. + +This setting asks crawlers to not index your instance. +This setting is enabled by default. Disable this setting to allow indexing. ### No follow -If indexing is allowed, crawlers will also attempt to follow any links on your page to further "discover" new parts of the Internet and on your website. -By default, this setting is enabled and will ask crawlers to not follow any links. -Crawler of different companies may behave differently when indexing is prohibited and following is allowed. + +Crawlers follow links to discover new pages. +This setting asks crawlers to not follow links. This setting is enabled by default. +Crawler behavior can differ between search engines. ### No translate -Google and Chrome on Android will prompt users to translate pages that are not in their device language. -This can lead to some shifting and can be undesired in some cases. By enabling this, -you ask crawlers to never translate and never prompt users for translation. -It should be noted that some browsers will ignore this and translate regardless. + +Some browsers offer to translate pages. +This setting asks crawlers to not offer translation. Some browsers ignore this setting. ### No site links search box -This setting is exclusive to Google. -[Google will create a box below your domain](https://developers.google.com/search/docs/appearance/structured-data/sitelinks-searchbox) that lists the paths that were discovered by the "folowing". -The user can search them directly in Google. -You can ask the crawlers to not allow searches by enabling this setting. -This setting is disabled by default. + +This setting applies to Google only. +Google can show a search box below your domain. +This setting asks Google to not show this box. This setting is disabled by default. ## Boards ### Global home board -You can set a global home board that will be used for all users that have not set a home board themselves. -It will also be used for anonymous users. -Note that the board must be public to be used as a global home board. + +You can set a global home board. +This board is the fallback for users without their own home board. It is also the fallback for anonymous users. +The board must be public. ### Global mobile board -You can set a global mobile board. It will be shown instead of the global home board on mobile devices that can be detected by Homarr. (Using the User-Agent header). -It will similar to the global home board be as fallback for normal users and for any anonymous users. -It also has to be public. +You can set a global mobile board. +Homarr shows this board on mobile devices instead of the global home board. +Homarr detects mobile devices with the User-Agent header. +The board must be public. ### App status #### Enable status by default -You can decide if the app status should be enabled by default when adding a new app to the board. +You can decide if app status is enabled when you add a new app. #### Force disable status -You can decide if the app status should be disabled for all boards. - -### +You can decide if app status is disabled on all boards. ## Appearance ### Default color scheme -You can set the default color scheme for all users that have not set a color scheme themselves. + +You can set the default color scheme for users without their own color scheme. ## Culture ### Default language -You can set the default language for all users that have not set a language themselves. \ No newline at end of file + +You can set the default language for users without their own language. From f76a0f240a016261d781949cac84102c700c83b1 Mon Sep 17 00:00:00 2001 From: Thomas Date: Thu, 6 Aug 2026 21:52:56 +0200 Subject: [PATCH 4/4] feat(apps): allow custom URI schemes in application links (#6521) --- apps/docs/docs/management/apps/index.mdx | 11 +++++++---- packages/api/src/router/app.ts | 2 +- packages/validation/src/app.ts | 3 +-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/docs/docs/management/apps/index.mdx b/apps/docs/docs/management/apps/index.mdx index 35c02a4fc..7c84df588 100644 --- a/apps/docs/docs/management/apps/index.mdx +++ b/apps/docs/docs/management/apps/index.mdx @@ -7,8 +7,8 @@ tags: - Ping --- -Apps are the building blocks of your Homarr board. -Most of the time they are a simple link to a website, but used on your board they also support a simple ping to check if your service is up. +Apps are the building blocks of your Homarr board. +Most of the time they are a simple link to a website, but used on your board they also support a simple ping to check if your service is up. Apps are used in the bookmark and app widget. ## Create an app @@ -27,7 +27,7 @@ The name must be at least one character long and can be chosen freely. ### Icon URL -This is the URL to the icon that will be displayed on the app widget. +This is the URL to the icon that will be displayed on the app widget. You can paste a direct HTTP(S) URL to an image, search the included icon repositories by name, or upload a local file through the upload button. Direct image URLs are used as entered and are not searched in the icon repositories. ![Icon URL](./img/app-icon-url.png) @@ -40,11 +40,14 @@ An optional description that can be shown as tooltip on the app widget. The URL to the website you want to link to. Clicking on the app widget or bookmark will open this URL. +You can also use custom URI schemes such as `vscode://file/path`, `microsoft-edge:https://example.com`, or `myapp://open` to link to local applications or third-party programs. We trust that you know what you are doing — only `javascript:` URIs are blocked for security reasons. + ### Ping URL -When you want to use another url for the simple status check you can set it here. +When you want to use another url for the simple status check you can set it here. This will allow you to set a different URL for the ping than the URL that is opened when clicking on the app widget. It's useful when you want to ping the service internally for example with a custom ip but open the website externally. +Note that the ping URL only accepts `http://` and `https://` URLs — custom URI schemes are not supported for pings. ## List of apps diff --git a/packages/api/src/router/app.ts b/packages/api/src/router/app.ts index 3d0285689..3c623b55a 100644 --- a/packages/api/src/router/app.ts +++ b/packages/api/src/router/app.ts @@ -173,7 +173,7 @@ export const appRouter = createTRPCRouter({ mcp: { enabled: true, description: - "Create a new app (bookmark/shortcut to a service). REQUIRED: name (string), iconUrl (icon URL string), href (app URL, http/https or blank). OPTIONAL: description (string or null), pingUrl (URL to check reachability, or empty string)", + "Create a new app (bookmark/shortcut to a service). REQUIRED: name (string), iconUrl (icon URL string), href (app URL or custom URI scheme such as vscode://, or blank). OPTIONAL: description (string or null), pingUrl (URL to check reachability, or empty string)", }, }) .mutation(async ({ ctx, input }) => { diff --git a/packages/validation/src/app.ts b/packages/validation/src/app.ts index 21a4335be..dfc7652a7 100644 --- a/packages/validation/src/app.ts +++ b/packages/validation/src/app.ts @@ -3,8 +3,7 @@ import { z } from "zod/v4"; export const appHrefSchema = z .string() .trim() - .url() - .regex(/^(?!javascript)[a-zA-Z]*:\/\//i) // javascript: is not allowed, i for case insensitive (so Javascript: is also not allowed) + .refine((val) => !/^javascript:/i.test(val), "JavaScript URIs are not permitted") .or(z.literal("")) .transform((value) => (value.length === 0 ? null : value)) .nullable();