diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 6336813b0..a192e95c5 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -31,6 +31,7 @@ body:
label: Version
description: What version of Homarr are you running?
options:
+ - 1.12.0
- 1.11.0
- 1.10.0
- 1.9.0
diff --git a/.github/renovate.json5 b/.github/renovate.json5
index e7e82ba4b..377002605 100644
--- a/.github/renovate.json5
+++ b/.github/renovate.json5
@@ -6,11 +6,6 @@
matchPackagePatterns: ["^@homarr/"],
enabled: false,
},
- // 15.2.0 crashes with turbopack error (panic)
- {
- matchPackagePatterns: ["^next$", "^@next/eslint-plugin-next$"],
- enabled: false,
- },
{
matchUpdateTypes: ["minor", "patch", "pin", "digest"],
automerge: true,
diff --git a/.run/All Tests.run.xml b/.run/All Tests.run.xml
new file mode 100644
index 000000000..ad69ee63f
--- /dev/null
+++ b/.run/All Tests.run.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.vscode/i18n-ally-custom-framework.yml b/.vscode/i18n-ally-custom-framework.yml
index 6cb944e00..1ee83bc7e 100644
--- a/.vscode/i18n-ally-custom-framework.yml
+++ b/.vscode/i18n-ally-custom-framework.yml
@@ -12,17 +12,17 @@ languageIds:
# You should unescape RegEx strings in order to fit in the YAML file
# To help with this, you can use https://www.freeformatter.com/json-escape.html
usageMatchRegex:
- # The following example shows how to detect `t("your.i18n.keys")`
- # the `{key}` will be placed by a proper keypath matching regex,
- # you can ignore it and use your own matching rules as well
+ # For direct t("your.i18n.keys") usage
- "[^\\w\\d]t\\(['\"`]({key})['\"`]"
+ # For variable t assigned from getScopedI18n or useScopedI18n
+ - "\\bt\\(['\"`]({key})['\"`]\\)"
# A RegEx to set a custom scope range. This scope will be used as a prefix when detecting keys
# and works like how the i18next framework identifies the namespace scope from the
# useTranslation() hook.
# You should unescape RegEx strings in order to fit in the YAML file
# To help with this, you can use https://www.freeformatter.com/json-escape.html
-scopeRangeRegex: "(getScopedI18n|useScopedI18n)\\(\\s*['\"](.*?)['\"]\\)"
+scopeRangeRegex: "(?:const|let|var)\\s+t\\s*=\\s*(?:await\\s+)?(?:getScopedI18n|useScopedI18n)\\(\\s*['\"](.*?)['\"]\\)"
# An array of strings containing refactor templates.
# The "$1" will be replaced by the keypath specified.
diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json
index 1fa69c38f..72afa9617 100644
--- a/apps/nextjs/package.json
+++ b/apps/nextjs/package.json
@@ -48,21 +48,21 @@
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0",
- "@mantine/colors-generator": "^7.17.2",
- "@mantine/core": "^7.17.2",
- "@mantine/dropzone": "^7.17.2",
- "@mantine/hooks": "^7.17.2",
- "@mantine/modals": "^7.17.2",
- "@mantine/tiptap": "^7.17.2",
+ "@mantine/colors-generator": "^7.17.3",
+ "@mantine/core": "^7.17.3",
+ "@mantine/dropzone": "^7.17.3",
+ "@mantine/hooks": "^7.17.3",
+ "@mantine/modals": "^7.17.3",
+ "@mantine/tiptap": "^7.17.3",
"@million/lint": "1.0.14",
"@tabler/icons-react": "^3.31.0",
- "@tanstack/react-query": "^5.69.0",
- "@tanstack/react-query-devtools": "^5.69.0",
- "@tanstack/react-query-next-experimental": "^5.69.0",
- "@trpc/client": "^11.0.0",
- "@trpc/next": "^11.0.0",
- "@trpc/react-query": "^11.0.0",
- "@trpc/server": "^11.0.0",
+ "@tanstack/react-query": "^5.70.0",
+ "@tanstack/react-query-devtools": "^5.70.0",
+ "@tanstack/react-query-next-experimental": "^5.70.0",
+ "@trpc/client": "^11.0.1",
+ "@trpc/next": "^11.0.1",
+ "@trpc/react-query": "^11.0.1",
+ "@trpc/server": "^11.0.1",
"@xterm/addon-canvas": "^0.7.0",
"@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "^5.5.0",
@@ -74,7 +74,7 @@
"glob": "^11.0.1",
"jotai": "^2.12.2",
"mantine-react-table": "2.0.0-beta.9",
- "next": "15.1.7",
+ "next": "15.2.4",
"postcss-preset-mantine": "^1.17.0",
"prismjs": "^1.30.0",
"react": "19.0.0",
@@ -83,7 +83,7 @@
"react-simple-code-editor": "^0.14.1",
"sass": "^1.86.0",
"superjson": "2.2.2",
- "swagger-ui-react": "^5.20.1",
+ "swagger-ui-react": "^5.20.2",
"use-deep-compare-effect": "^1.8.1",
"zod": "^3.24.2"
},
@@ -92,13 +92,13 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "3.1.1",
- "@types/node": "^22.13.11",
+ "@types/node": "^22.13.14",
"@types/prismjs": "^1.26.5",
"@types/react": "19.0.12",
"@types/react-dom": "19.0.4",
"@types/swagger-ui-react": "^5.18.0",
"concurrently": "^9.1.2",
- "eslint": "^9.22.0",
+ "eslint": "^9.23.0",
"node-loader": "^2.1.0",
"prettier": "^3.5.3",
"typescript": "^5.8.2"
diff --git a/apps/nextjs/src/app/[locale]/init/_steps/user/init-user-form.tsx b/apps/nextjs/src/app/[locale]/init/_steps/user/init-user-form.tsx
index e094ce4ce..2dd2df722 100644
--- a/apps/nextjs/src/app/[locale]/init/_steps/user/init-user-form.tsx
+++ b/apps/nextjs/src/app/[locale]/init/_steps/user/init-user-form.tsx
@@ -4,6 +4,7 @@ import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
import type { z } from "zod";
import { clientApi } from "@homarr/api/client";
+import { signIn } from "@homarr/auth/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
@@ -30,6 +31,13 @@ export const InitUserForm = () => {
title: tUser("notification.success.title"),
message: tUser("notification.success.message"),
});
+
+ await signIn("credentials", {
+ name: values.username,
+ password: values.password,
+ redirect: false,
+ });
+
await revalidatePathActionAsync("/init");
},
onError: (error) => {
diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/new/page.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/new/[id]/page.tsx
similarity index 59%
rename from apps/nextjs/src/app/[locale]/manage/integrations/new/page.tsx
rename to apps/nextjs/src/app/[locale]/manage/integrations/new/[id]/page.tsx
index dba8e5c1f..4622faa1e 100644
--- a/apps/nextjs/src/app/[locale]/manage/integrations/new/page.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/integrations/new/[id]/page.tsx
@@ -3,49 +3,55 @@ import { Container, Group, Stack, Title } from "@mantine/core";
import { z } from "zod";
import { auth } from "@homarr/auth/next";
-import type { IntegrationKind } from "@homarr/definitions";
import { getIntegrationName, integrationKinds } from "@homarr/definitions";
import { getScopedI18n } from "@homarr/translation/server";
import { IntegrationAvatar } from "@homarr/ui";
import type { validation } from "@homarr/validation";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
-import { NewIntegrationForm } from "./_integration-new-form";
+import { NewIntegrationForm } from "../_integration-new-form";
-interface NewIntegrationPageProps {
- searchParams: Promise<
- Partial> & {
- kind: IntegrationKind;
- }
- >;
+interface NewIntegrationByIdPageProps {
+ params: {
+ id: string;
+ };
+ searchParams: Partial>;
}
-export default async function IntegrationsNewPage(props: NewIntegrationPageProps) {
- const searchParams = await props.searchParams;
+export function generateStaticParams() {
+ return integrationKinds.map((kind) => ({
+ id: kind,
+ }));
+}
+
+export default async function IntegrationNewByIdPage(props: NewIntegrationByIdPageProps) {
+ const { id } = props.params;
const session = await auth();
+
if (!session?.user.permissions.includes("integration-create")) {
notFound();
}
- const result = z.enum(integrationKinds).safeParse(searchParams.kind);
+ const result = z.enum(integrationKinds).safeParse(id);
if (!result.success) {
notFound();
}
const tCreate = await getScopedI18n("integration.page.create");
-
const currentKind = result.data;
+ const dynamicMappings = new Map([[id, getIntegrationName(currentKind)]]);
+
return (
<>
-
+
{tCreate("title", { name: getIntegrationName(currentKind) })}
-
+
>
diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-dropdown.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-dropdown.tsx
index a697b9270..af94f6642 100644
--- a/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-dropdown.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-dropdown.tsx
@@ -39,7 +39,7 @@ export const IntegrationCreateDropdownContent = () => {
{filteredKinds.length > 0 ? (
{filteredKinds.map((kind) => (
-
+
{getIntegrationName(kind)}
diff --git a/apps/nextjs/src/app/[locale]/manage/tools/tasks/_components/jobs-list.tsx b/apps/nextjs/src/app/[locale]/manage/tools/tasks/_components/jobs-list.tsx
index c1150622c..53058ad80 100644
--- a/apps/nextjs/src/app/[locale]/manage/tools/tasks/_components/jobs-list.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/tools/tasks/_components/jobs-list.tsx
@@ -66,15 +66,17 @@ export const JobsList = ({ initialJobs }: JobsListProps) => {
{job.status && }
- handleJobTrigger(job)}
- disabled={job.status?.status === "running"}
- variant={"default"}
- size={"xl"}
- radius={"xl"}
- >
-
-
+ {!job.job.preventManualExecution && (
+ handleJobTrigger(job)}
+ disabled={job.status?.status === "running"}
+ variant={"default"}
+ size={"xl"}
+ radius={"xl"}
+ >
+
+
+ )}
))}
diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx
index add232243..ee960b0f5 100644
--- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx
@@ -118,15 +118,13 @@ export default async function EditUserPage(props: Props) {
- {isCredentialsUser && (
-
- }
- />
-
- )}
+
+ }
+ />
+
);
}
diff --git a/apps/nextjs/src/app/api/[...trpc]/route.ts b/apps/nextjs/src/app/api/[...trpc]/route.ts
index 42a09e316..ea992afda 100644
--- a/apps/nextjs/src/app/api/[...trpc]/route.ts
+++ b/apps/nextjs/src/app/api/[...trpc]/route.ts
@@ -26,6 +26,9 @@ const handlerAsync = async (req: NextRequest) => {
endpoint: "/",
router: appRouter,
createContext: () => createTRPCContext({ session, headers: req.headers }),
+ onError({ error, path, type }) {
+ logger.error(new Error(`tRPC Error with ${type} on '${path}'`, { cause: error.cause }));
+ },
});
};
diff --git a/apps/nextjs/src/app/api/trpc/[trpc]/route.ts b/apps/nextjs/src/app/api/trpc/[trpc]/route.ts
index 66aa2ff71..f35afb7a3 100644
--- a/apps/nextjs/src/app/api/trpc/[trpc]/route.ts
+++ b/apps/nextjs/src/app/api/trpc/[trpc]/route.ts
@@ -31,9 +31,7 @@ const handler = auth(async (req) => {
req,
createContext: () => createTRPCContext({ session: req.auth, headers: req.headers }),
onError({ error, path, type }) {
- logger.error(
- `tRPC Error with ${type} on '${path}': (${error.code}) - ${error.message}\n${error.stack}\n${error.cause}`,
- );
+ logger.error(new Error(`tRPC Error with ${type} on '${path}'`, { cause: error.cause }));
},
});
diff --git a/apps/nextjs/src/components/board/items/actions/test/mocks/dynamic-section-mock.ts b/apps/nextjs/src/components/board/items/actions/test/mocks/dynamic-section-mock.ts
index 7de8db63c..553deda9e 100644
--- a/apps/nextjs/src/components/board/items/actions/test/mocks/dynamic-section-mock.ts
+++ b/apps/nextjs/src/components/board/items/actions/test/mocks/dynamic-section-mock.ts
@@ -10,6 +10,7 @@ export class DynamicSectionMockBuilder {
id: createId(),
kind: "dynamic",
options: {
+ title: "",
borderColor: "",
},
layouts: [],
diff --git a/apps/nextjs/src/components/board/items/item-content.tsx b/apps/nextjs/src/components/board/items/item-content.tsx
index 709d52182..7ee901ac8 100644
--- a/apps/nextjs/src/components/board/items/item-content.tsx
+++ b/apps/nextjs/src/components/board/items/item-content.tsx
@@ -5,6 +5,8 @@ import combineClasses from "clsx";
import { NoIntegrationSelectedError } from "node_modules/@homarr/widgets/src/errors";
import { ErrorBoundary } from "react-error-boundary";
+import { useSession } from "@homarr/auth/client";
+import { isWidgetRestricted } from "@homarr/auth/shared";
import { useRequiredBoard } from "@homarr/boards/context";
import { useEditMode } from "@homarr/boards/edit-mode";
import { useSettings } from "@homarr/settings";
@@ -15,6 +17,7 @@ import type { SectionItem } from "~/app/[locale]/boards/_types";
import classes from "../sections/item.module.css";
import { useItemActions } from "./item-actions";
import { BoardItemMenu } from "./item-menu";
+import { RestrictedWidgetContent } from "./restricted";
interface BoardItemContentProps {
item: SectionItem;
@@ -59,6 +62,7 @@ interface InnerContentProps {
const InnerContent = ({ item, ...dimensions }: InnerContentProps) => {
const settings = useSettings();
const board = useRequiredBoard();
+ const { data: session } = useSession();
const [isEditMode] = useEditMode();
const Comp = loadWidgetDynamic(item.kind);
const { definition } = widgetImports[item.kind];
@@ -70,6 +74,16 @@ const InnerContent = ({ item, ...dimensions }: InnerContentProps) => {
const widgetSupportsIntegrations =
"supportedIntegrations" in definition && definition.supportedIntegrations.length >= 1;
+ if (
+ isWidgetRestricted({
+ definition,
+ user: session?.user ?? null,
+ check: (level) => level === "all",
+ })
+ ) {
+ return ;
+ }
+
return (
{({ reset }) => (
diff --git a/apps/nextjs/src/components/board/items/item-menu.tsx b/apps/nextjs/src/components/board/items/item-menu.tsx
index e7072fea4..771279679 100644
--- a/apps/nextjs/src/components/board/items/item-menu.tsx
+++ b/apps/nextjs/src/components/board/items/item-menu.tsx
@@ -3,6 +3,8 @@ import { ActionIcon, Menu } from "@mantine/core";
import { IconCopy, IconDotsVertical, IconLayoutKanban, IconPencil, IconTrash } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
+import { useSession } from "@homarr/auth/client";
+import { isWidgetRestricted } from "@homarr/auth/shared";
import { useEditMode } from "@homarr/boards/edit-mode";
import { useConfirmModal, useModalAction } from "@homarr/modals";
import { useSettings } from "@homarr/settings";
@@ -37,6 +39,7 @@ export const BoardItemMenu = ({
const currentDefinition = useMemo(() => widgetImports[item.kind].definition, [item.kind]);
const { gridstack } = useSectionContext().refs;
const settings = useSettings();
+ const { data: session } = useSession();
// Reset error boundary on next render if item has been edited
useEffect(() => {
@@ -91,6 +94,16 @@ export const BoardItemMenu = ({
});
};
+ if (
+ isWidgetRestricted({
+ definition: currentDefinition,
+ user: session?.user ?? null,
+ check: (level) => level !== "none",
+ })
+ ) {
+ return null;
+ }
+
return (