Compare commits

...
Author SHA1 Message Date
Thomas Camlong 15feac9aaa fix(old-import): add missing withBorder mapping for bookmarks widget
Restores typecheck after dev merge added the bookmarks withBorder option.
2026-06-04 23:14:38 +02:00
Thomas Camlong 5b1e3ce80e Merge branch 'dev' into feat/board-cache
Resolve _creator.tsx conflict: keep React cache via getBoardForParams.
2026-06-04 22:59:28 +02:00
Thomas 41fe5928f1 fix: add bookmark widget option to toggle card borders (#5854) 2026-06-04 11:07:31 +02:00
homarr-renovate[bot] 6eb59d6f53 chore(deps): update tiptap monorepo to v3.24.0 (#5858)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-04 08:58:12 +00:00
Afonso Jorge Ramos 2c6b7a818e fix(immich): respect app color scheme in server-stats widget (#5859) 2026-06-04 09:50:02 +02:00
Thomas fd10788838 fix(auth): relax password complexity requirements to suggestions-only (#5764) 2026-06-04 07:56:34 +02:00
Thomas fe4598f722 fix(docker): restore container selection toolbar broken by Mantine v9 (#5855) 2026-06-03 10:03:37 +02:00
Thomas a755692d81 fix(ui): restore pre-v9 light variant colors (#5856) 2026-06-03 06:49:38 +00:00
homarr-renovate[bot] 3ff0d72ba1 chore(deps): update dependency next-intl to v4.13.0 (#5853)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-03 04:21:57 +00:00
homarr-renovate[bot] a63a191203 chore(deps): update turbo monorepo to ^2.9.16 (#5851)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-02 23:54:09 +00:00
homarr-renovate[bot] 1be906a0cd chore(deps): update dependency isomorphic-dompurify to ^3.15.0 (#5852)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-02 21:55:53 +00:00
homarr-renovate[bot] 63d8fa8c26 chore(deps): update dependency @ctrl/qbittorrent to ^9.12.2 (#5834)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-02 18:47:29 +00:00
homarr-renovate[bot] b5dd6ae8ef chore(deps): update babel monorepo to >=7.29.7 (#5830)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-02 14:11:26 +00:00
homarr-renovate[bot] d540ad374a chore(deps): update pnpm to v10.34.0 (#5846)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-02 09:17:24 +00:00
homarr-renovate[bot] 6a971c1986 chore(deps): update mantine monorepo to ^9.2.2 (#5844)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-02 04:13:28 +00:00
homarr-crowdin[bot] d86fe3ddf0 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2026-06-02 02:23:34 +00:00
homarr-renovate[bot] 06db8960a7 chore(deps): update turbo monorepo to ^2.9.15 (#5841)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-01 23:12:09 +00:00
homarr-renovate[bot] a5719acf34 chore(deps): update dependency tmp@<=0.2.3 to >=0.2.7 (#5848)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-01 21:06:21 +00:00
homarr-renovate[bot] d1d597c8e1 chore(deps): update testcontainers-node monorepo to ^12.0.1 (#5845)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-01 16:48:41 +00:00
homarr-renovate[bot] 657e42e4bb chore(deps): update dependency mysql2 to v3.22.4 (#5839)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-31 18:12:08 +00:00
homarr-renovate[bot] 256ebcb864 chore(deps): update dependency oxlint to ^1.67.0 (#5837)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-31 16:18:04 +00:00
homarr-renovate[bot] 72e86dee01 chore(deps): update dependency ioredis to v5.11.0 (#5838)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-31 14:53:18 +00:00
homarr-renovate[bot] b7910b16e0 chore(deps): update dependency oxfmt to ^0.52.0 (#5836)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-31 11:08:29 +00:00
homarr-renovate[bot] 5a83bb6e39 chore(deps): update dependency dayjs to ^1.11.21 (#5835)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-31 08:54:20 +00:00
homarr-renovate[bot] 433b66582d chore(deps): update dependency hono@<4.6.5 to >=4.12.23 (#5828)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-30 09:25:22 +00:00
homarr-renovate[bot] acbcb6818b chore(deps): update dependency react-error-boundary to ^6.1.2 (#5829)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-30 06:54:05 +00:00
homarr-crowdin[bot] 07d991a0a1 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2026-05-30 05:27:45 +00:00
homarr-renovate[bot] 92364e9574 chore(deps): update tanstack-query monorepo to ^5.100.14 (#5813)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-29 21:15:51 +00:00
Thomas Camlong 884fde245b fix(boards): replace global cache with React cache() per-request dedup
The unstable_cache approach bypassed throwIfActionForbiddenAsync and
dropped user-specific data (permissions, collapse states). Use React
cache() to deduplicate getInitialBoard within a single request instead,
preserving auth checks and all user data while reducing DB calls from
9 to 3 per board page load.
2026-05-25 11:01:07 +02:00
Thomas Camlong 689da2fb91 fix: format files and fix revalidateTag signature for Next 16
- Run prettier on all modified files
- Add required cache profile arg to revalidateTag
- Suppress require-await for server action (must be async)
2026-05-18 15:01:01 +02:00
Thomas Camlong 22af9e2c3e feat(boards): cache named board data with tag-based invalidation
Named boards are now served from Next.js unstable_cache (1h TTL,
tag `board-{name}`). A new `getSharedBoardWithWhereAsync` helper
fetches the board without per-user data so the result is safe to
share across requests.

Cache is busted via `invalidateBoardCacheAsync` (revalidateTag +
revalidatePath) from board save, layout save, and partial settings
mutations.
2026-05-18 14:51:44 +02:00
Thomas Camlong 2c388bd449 perf(boards): parallelize board page data fetching
Board fetch + auth() now run concurrently via Promise.all instead of
sequentially. Widget prefetches for all kinds also run in parallel
alongside integration permission loading.
2026-05-18 14:49:07 +02:00
56 changed files with 1194 additions and 1154 deletions

No files matched your search

@@ -2,7 +2,7 @@
import type { PropsWithChildren } from "react"; import type { PropsWithChildren } from "react";
import type { MantineColorScheme, MantineColorSchemeManager } from "@mantine/core"; import type { MantineColorScheme, MantineColorSchemeManager } from "@mantine/core";
import { DirectionProvider, MantineProvider } from "@mantine/core"; import { DirectionProvider, MantineProvider, v8CssVariablesResolver } from "@mantine/core";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
@@ -18,7 +18,12 @@ export const CustomMantineProvider = ({
const manager = useColorSchemeManager(); const manager = useColorSchemeManager();
return ( return (
<DirectionProvider> <DirectionProvider>
<MantineProvider defaultColorScheme={defaultColorScheme} colorSchemeManager={manager} theme={theme}> <MantineProvider
defaultColorScheme={defaultColorScheme}
colorSchemeManager={manager}
theme={theme}
cssVariablesResolver={v8CssVariablesResolver}
>
{children} {children}
</MantineProvider> </MantineProvider>
</DirectionProvider> </DirectionProvider>
@@ -1,3 +1,4 @@
import { cache } from "react";
import type { Metadata } from "next"; import type { Metadata } from "next";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
@@ -34,17 +35,20 @@ interface Props<TParams extends Params> {
export const createBoardContentPage = <TParams extends Record<string, unknown>>({ export const createBoardContentPage = <TParams extends Record<string, unknown>>({
getInitialBoardAsync: getInitialBoard, getInitialBoardAsync: getInitialBoard,
}: Props<TParams>) => { }: Props<TParams>) => {
const getCachedBoard = cache((_paramsKey: string, params: TParams) => getInitialBoard(params));
const getBoardForParams = (params: TParams) => getCachedBoard(JSON.stringify(params), params);
return { return {
layout: createBoardLayout({ layout: createBoardLayout({
headerActions: <BoardContentHeaderActions />, headerActions: <BoardContentHeaderActions />,
getInitialBoardAsync: getInitialBoard, getInitialBoardAsync: getBoardForParams,
}), }),
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
page: async ({ params }: { params: Promise<TParams> }) => { page: async ({ params }: { params: Promise<TParams> }) => {
const resolvedParams = await params; const resolvedParams = await params;
const queryClient = getQueryClient(); const queryClient = getQueryClient();
const [board, session] = await Promise.all([getInitialBoard(resolvedParams), auth()]); const [board, session] = await Promise.all([getBoardForParams(resolvedParams), auth()]);
const itemsMap = board.items.reduce((acc, item) => { const itemsMap = board.items.reduce((acc, item) => {
const existing = acc.get(item.kind); const existing = acc.get(item.kind);
@@ -81,7 +85,7 @@ export const createBoardContentPage = <TParams extends Record<string, unknown>>(
}, },
generateMetadataAsync: async ({ params }: { params: Promise<TParams> }): Promise<Metadata> => { generateMetadataAsync: async ({ params }: { params: Promise<TParams> }): Promise<Metadata> => {
try { try {
const board = await getInitialBoard(await params); const board = await getBoardForParams(await params);
const t = await getI18n(); const t = await getI18n();
return { return {
@@ -97,7 +101,6 @@ export const createBoardContentPage = <TParams extends Record<string, unknown>>(
}, },
}; };
} catch (error) { } catch (error) {
// Ignore not found errors and return empty metadata
if (error instanceof TRPCError && error.code === "NOT_FOUND") { if (error instanceof TRPCError && error.code === "NOT_FOUND") {
return {}; return {};
} }
@@ -2,7 +2,7 @@
import type { PropsWithChildren } from "react"; import type { PropsWithChildren } from "react";
import type { MantineColorsTuple } from "@mantine/core"; import type { MantineColorsTuple } from "@mantine/core";
import { colorsTuple, createTheme, darken, lighten, MantineProvider, rem } from "@mantine/core"; import { colorsTuple, createTheme, darken, lighten, MantineProvider, rem, v8CssVariablesResolver } from "@mantine/core";
import { useRequiredBoard } from "@homarr/boards/context"; import { useRequiredBoard } from "@homarr/boards/context";
import type { ColorScheme } from "@homarr/definitions"; import type { ColorScheme } from "@homarr/definitions";
@@ -33,7 +33,12 @@ export const BoardMantineProvider = ({
}); });
return ( return (
<MantineProvider defaultColorScheme={defaultColorScheme} theme={theme} colorSchemeManager={colorSchemeManager}> <MantineProvider
defaultColorScheme={defaultColorScheme}
theme={theme}
colorSchemeManager={colorSchemeManager}
cssVariablesResolver={v8CssVariablesResolver}
>
{children} {children}
</MantineProvider> </MantineProvider>
); );
@@ -1,15 +1,15 @@
"use client"; "use client";
import { Button, PasswordInput, Stack, TextInput } from "@mantine/core"; import { Button, Stack, TextInput } from "@mantine/core";
import type { z } from "zod/v4"; import type { z } from "zod/v4";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { signIn } from "@homarr/auth/client"; import { signIn } from "@homarr/auth/client";
import { revalidatePathActionAsync } from "@homarr/common/client"; import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { UserCreatePasswordFields } from "@homarr/forms-collection";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import { CustomPasswordInput } from "@homarr/ui";
import { userInitSchema } from "@homarr/validation/user"; import { userInitSchema } from "@homarr/validation/user";
export const InitUserForm = () => { export const InitUserForm = () => {
@@ -59,12 +59,10 @@ export const InitUserForm = () => {
> >
<Stack gap="lg"> <Stack gap="lg">
<TextInput label={t("field.username.label")} {...form.getInputProps("username")} /> <TextInput label={t("field.username.label")} {...form.getInputProps("username")} />
<CustomPasswordInput <UserCreatePasswordFields
withPasswordRequirements passwordInputProps={form.getInputProps("password")}
label={t("field.password.label")} confirmPasswordInputProps={form.getInputProps("confirmPassword")}
{...form.getInputProps("password")}
/> />
<PasswordInput label={t("field.passwordConfirm.label")} {...form.getInputProps("confirmPassword")} />
<Button type="submit" fullWidth loading={isPending}> <Button type="submit" fullWidth loading={isPending}>
{t("action.create")} {t("action.create")}
</Button> </Button>
@@ -107,6 +107,23 @@ export function DockerTable(initialData: RouterOutputs["docker"]["getContainers"
refetchOnReconnect: false, refetchOnReconnect: false,
}); });
const relativeTime = useTimeAgo(data.timestamp); const relativeTime = useTimeAgo(data.timestamp);
const utils = clientApi.useUtils();
const { mutate, isPending } = clientApi.docker.invalidate.useMutation({
async onSuccess() {
await utils.docker.getContainers.invalidate();
showSuccessNotification({
title: tDocker("action.refresh.notification.success.title"),
message: tDocker("action.refresh.notification.success.message"),
});
},
onError() {
showErrorNotification({
title: tDocker("action.refresh.notification.error.title"),
message: tDocker("action.refresh.notification.error.message"),
});
},
});
const table = useTranslatedMantineReactTable({ const table = useTranslatedMantineReactTable({
data: data.containers, data: data.containers,
enableDensityToggle: false, enableDensityToggle: false,
@@ -125,35 +142,11 @@ export function DockerTable(initialData: RouterOutputs["docker"]["getContainers"
}, },
initialState: { density: "xs", showGlobalFilter: true }, initialState: { density: "xs", showGlobalFilter: true },
renderTopToolbarCustomActions: () => { renderTopToolbarCustomActions: () => (
const utils = clientApi.useUtils(); <Button variant="default" rightSection={<IconRefresh size="1rem" />} onClick={() => mutate()} loading={isPending}>
const { mutate, isPending } = clientApi.docker.invalidate.useMutation({ {tDocker("action.refresh.label")}
async onSuccess() { </Button>
await utils.docker.getContainers.invalidate(); ),
showSuccessNotification({
title: tDocker("action.refresh.notification.success.title"),
message: tDocker("action.refresh.notification.success.message"),
});
},
onError() {
showErrorNotification({
title: tDocker("action.refresh.notification.error.title"),
message: tDocker("action.refresh.notification.error.message"),
});
},
});
return (
<Button
variant="default"
rightSection={<IconRefresh size="1rem" />}
onClick={() => mutate()}
loading={isPending}
>
{tDocker("action.refresh.label")}
</Button>
);
},
renderToolbarAlertBannerContent: ({ groupedAlert, table }) => { renderToolbarAlertBannerContent: ({ groupedAlert, table }) => {
const dockerContainers = table.getSelectedRowModel().rows.map((row) => row.original); const dockerContainers = table.getSelectedRowModel().rows.map((row) => row.original);
return ( return (
@@ -1,20 +1,7 @@
"use client"; "use client";
import { startTransition, useCallback, useState } from "react"; import { startTransition, useCallback, useState } from "react";
import { import { Badge, Button, Card, Group, Stack, Stepper, Table, Text, TextInput, Title, Tooltip } from "@mantine/core";
Badge,
Button,
Card,
Group,
PasswordInput,
Stack,
Stepper,
Table,
Text,
TextInput,
Title,
Tooltip,
} from "@mantine/core";
import { useListState } from "@mantine/hooks"; import { useListState } from "@mantine/hooks";
import { IconPlus, IconUserCheck } from "@tabler/icons-react"; import { IconPlus, IconUserCheck } from "@tabler/icons-react";
import { z } from "zod/v4"; import { z } from "zod/v4";
@@ -24,10 +11,11 @@ import type { GroupPermissionKey } from "@homarr/definitions";
import { everyoneGroup, groupPermissions } from "@homarr/definitions"; import { everyoneGroup, groupPermissions } from "@homarr/definitions";
import type { IsValid } from "@homarr/form"; import type { IsValid } from "@homarr/form";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { UserCreatePasswordFields } from "@homarr/forms-collection";
import { useModalAction } from "@homarr/modals"; import { useModalAction } from "@homarr/modals";
import { showErrorNotification } from "@homarr/notifications"; import { showErrorNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client"; import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { CustomPasswordInput, UserAvatar } from "@homarr/ui"; import { UserAvatar } from "@homarr/ui";
import { createCustomErrorParams } from "@homarr/validation/form/i18n"; import { createCustomErrorParams } from "@homarr/validation/form/i18n";
import { userPasswordSchema } from "@homarr/validation/user"; import { userPasswordSchema } from "@homarr/validation/user";
@@ -167,18 +155,10 @@ export const UserCreateStepperComponent = ({ initialGroups }: UserCreateStepperC
<form> <form>
<Card p="xl"> <Card p="xl">
<Stack gap="md"> <Stack gap="md">
<CustomPasswordInput <UserCreatePasswordFields
withPasswordRequirements
label={tUserField("password.label")}
variant="filled" variant="filled"
withAsterisk passwordInputProps={securityForm.getInputProps("password")}
{...securityForm.getInputProps("password")} confirmPasswordInputProps={securityForm.getInputProps("confirmPassword")}
/>
<PasswordInput
label={tUserField("passwordConfirm.label")}
variant="filled"
withAsterisk
{...securityForm.getInputProps("confirmPassword")}
/> />
</Stack> </Stack>
</Card> </Card>
+2 -2
View File
@@ -6,9 +6,9 @@
}, },
"engines": { "engines": {
"node": ">=24.16.0", "node": ">=24.16.0",
"pnpm": ">=10.33.4" "pnpm": ">=10.34.1"
}, },
"packageManager": "pnpm@10.33.4", "packageManager": "pnpm@10.34.1",
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
"better-sqlite3" "better-sqlite3"
+6 -6
View File
@@ -60,9 +60,9 @@
}, },
"engines": { "engines": {
"node": ">=24.16.0", "node": ">=24.16.0",
"pnpm": ">=10.33.4" "pnpm": ">=10.34.1"
}, },
"packageManager": "pnpm@10.33.4", "packageManager": "pnpm@10.34.1",
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
"@tree-sitter-grammars/tree-sitter-yaml", "@tree-sitter-grammars/tree-sitter-yaml",
@@ -76,14 +76,14 @@
"tree-sitter-json" "tree-sitter-json"
], ],
"overrides": { "overrides": {
"@babel/helpers@<7.26.10": ">=7.29.2", "@babel/helpers@<7.26.10": ">=7.29.7",
"@babel/runtime@<7.26.10": ">=7.29.2", "@babel/runtime@<7.26.10": ">=7.29.7",
"axios@>=1.0.0 <1.8.2": ">=1.16.1", "axios@>=1.0.0 <1.8.2": ">=1.16.1",
"brace-expansion@>=2.0.0 <=2.0.1": ">=5.0.6", "brace-expansion@>=2.0.0 <=2.0.1": ">=5.0.6",
"brace-expansion@>=1.0.0 <=1.1.11": ">=5.0.6", "brace-expansion@>=1.0.0 <=1.1.11": ">=5.0.6",
"esbuild@<=0.24.2": ">=0.28.0", "esbuild@<=0.24.2": ">=0.28.0",
"form-data@>=4.0.0 <4.0.4": ">=4.0.5", "form-data@>=4.0.0 <4.0.4": ">=4.0.5",
"hono@<4.6.5": ">=4.12.22", "hono@<4.6.5": ">=4.12.23",
"linkifyjs@<4.3.2": ">=4.3.3", "linkifyjs@<4.3.2": ">=4.3.3",
"nanoid@>=4.0.0 <5.0.9": ">=5.1.11", "nanoid@>=4.0.0 <5.0.9": ">=5.1.11",
"prismjs@<1.30.0": ">=1.30.0", "prismjs@<1.30.0": ">=1.30.0",
@@ -93,7 +93,7 @@
"sha.js@<=2.4.11": ">=2.4.12", "sha.js@<=2.4.11": ">=2.4.12",
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.2", "tar-fs@>=3.0.0 <3.0.9": ">=3.1.2",
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.2", "tar-fs@>=2.0.0 <2.1.3": ">=3.1.2",
"tmp@<=0.2.3": ">=0.2.5", "tmp@<=0.2.3": ">=0.2.7",
"vite@>=5.0.0 <=5.4.18": ">=8.0.14" "vite@>=5.0.0 <=5.4.18": ">=8.0.14"
}, },
"patchedDependencies": { "patchedDependencies": {
+50 -8
View File
@@ -81,13 +81,7 @@ describe("initUser should initialize the first user", () => {
await expect(actAsync()).rejects.toThrow("passwordsDoNotMatch"); await expect(actAsync()).rejects.toThrow("passwordsDoNotMatch");
}); });
it.each([ it.each([["aB2%"], ["short"]])("should reject passwords shorter than 8 characters for '%s'", async (password) => {
["aB2%"], // too short
["abc123DEF"], // does not contain special characters
["abcDEFghi+"], // does not contain numbers
["ABC123+/-"], // does not contain lowercase
["abc123+/-"], // does not contain uppercase
])("should throw error that password requirements do not match for '%s' as password", async (password) => {
const db = createDb(); const db = createDb();
await createOnboardingStepAsync(db, "user"); await createOnboardingStepAsync(db, "user");
const caller = userRouter.createCaller({ const caller = userRouter.createCaller({
@@ -103,7 +97,55 @@ describe("initUser should initialize the first user", () => {
confirmPassword: password, confirmPassword: password,
}); });
await expect(actAsync()).rejects.toThrow("passwordRequirements"); await expect(actAsync()).rejects.toThrow();
});
it("should accept passwords without complexity requirements", async () => {
const db = createDb();
await createOnboardingStepAsync(db, "user");
const caller = userRouter.createCaller({
db,
deviceType: undefined,
session: null,
});
await caller.initUser({
username: "test",
password: "abc123DEF",
confirmPassword: "abc123DEF",
});
const user = await db.query.users.findFirst({
columns: {
id: true,
},
});
expect(user).toBeDefined();
});
it("should accept passwords with special characters like LoveHomarr<3", async () => {
const db = createDb();
await createOnboardingStepAsync(db, "user");
const caller = userRouter.createCaller({
db,
deviceType: undefined,
session: null,
});
await caller.initUser({
username: "test",
password: "LoveHomarr<3",
confirmPassword: "LoveHomarr<3",
});
const user = await db.query.users.findFirst({
columns: {
id: true,
},
});
expect(user).toBeDefined();
}); });
}); });
+2
View File
@@ -5,3 +5,5 @@ export * from "./icon-picker/icon-picker";
export * from "./new-app/icon-matcher"; export * from "./new-app/icon-matcher";
export * from "./upload-media/upload-media"; export * from "./upload-media/upload-media";
export * from "./user-create/user-create-password-fields";
@@ -0,0 +1,33 @@
"use client";
import { PasswordInput, Stack } from "@mantine/core";
import type { PasswordInputProps } from "@mantine/core";
import { useScopedI18n } from "@homarr/translation/client";
import { CustomPasswordInput } from "@homarr/ui";
interface UserCreatePasswordFieldsProps {
passwordInputProps: PasswordInputProps;
confirmPasswordInputProps: PasswordInputProps;
variant?: PasswordInputProps["variant"];
}
export const UserCreatePasswordFields = ({
passwordInputProps,
confirmPasswordInputProps,
variant,
}: UserCreatePasswordFieldsProps) => {
const t = useScopedI18n("user.field");
return (
<Stack gap="md">
<CustomPasswordInput
withPasswordRequirements
label={t("password.label")}
variant={variant}
{...passwordInputProps}
/>
<PasswordInput label={t("passwordConfirm.label")} variant={variant} {...confirmPasswordInputProps} />
</Stack>
);
};
@@ -44,6 +44,7 @@ const optionMapping: OptionMapping = {
hideIcon: (oldOptions) => oldOptions.items.some((item) => item.hideIcon), hideIcon: (oldOptions) => oldOptions.items.some((item) => item.hideIcon),
hideHostname: (oldOptions) => oldOptions.items.some((item) => item.hideHostname), hideHostname: (oldOptions) => oldOptions.items.some((item) => item.hideHostname),
openNewTab: (oldOptions) => oldOptions.items.some((item) => item.openNewTab), openNewTab: (oldOptions) => oldOptions.items.some((item) => item.openNewTab),
withBorder: () => undefined,
}, },
calendar: { calendar: {
releaseType: (oldOptions) => [oldOptions.radarrReleaseType], releaseType: (oldOptions) => [oldOptions.radarrReleaseType],
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Contrasenya", "label": "Contrasenya",
"requirement": { "suggestion": {
"length": "Inclou com a mínim 8 caràcters", "length": "Inclou com a mínim 8 caràcters",
"lowercase": "Inclou minúscules", "lowercase": "Inclou minúscules",
"uppercase": "Inclou majúscules", "uppercase": "Inclou majúscules",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "密码", "label": "密码",
"requirement": { "suggestion": {
"length": "至少包含 8 个字符", "length": "至少包含 8 个字符",
"lowercase": "包含小写字母", "lowercase": "包含小写字母",
"uppercase": "包含大写字母", "uppercase": "包含大写字母",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "crwdns26:0crwdne26:0", "label": "crwdns26:0crwdne26:0",
"requirement": { "suggestion": {
"length": "crwdns28:0crwdne28:0", "length": "crwdns28:0crwdne28:0",
"lowercase": "crwdns30:0crwdne30:0", "lowercase": "crwdns30:0crwdne30:0",
"uppercase": "crwdns32:0crwdne32:0", "uppercase": "crwdns32:0crwdne32:0",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Heslo", "label": "Heslo",
"requirement": { "suggestion": {
"length": "Obsahuje alespoň 8 znaků", "length": "Obsahuje alespoň 8 znaků",
"lowercase": "Obsahuje malé písmeno", "lowercase": "Obsahuje malé písmeno",
"uppercase": "Obsahuje velké písmeno", "uppercase": "Obsahuje velké písmeno",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Adgangskode", "label": "Adgangskode",
"requirement": { "suggestion": {
"length": "Inkluderer mindst 8 tegn", "length": "Inkluderer mindst 8 tegn",
"lowercase": "Inkluderer små bogstaver", "lowercase": "Inkluderer små bogstaver",
"uppercase": "Inkluderer store bogstaver", "uppercase": "Inkluderer store bogstaver",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Passwort", "label": "Passwort",
"requirement": { "suggestion": {
"length": "Enthält mindestens 8 Zeichen", "length": "Enthält mindestens 8 Zeichen",
"lowercase": "Enthält Kleinbuchstaben", "lowercase": "Enthält Kleinbuchstaben",
"uppercase": "Enthält Großbuchstaben", "uppercase": "Enthält Großbuchstaben",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Passwort", "label": "Passwort",
"requirement": { "suggestion": {
"length": "Enthält mindestens 8 Zeichen", "length": "Enthält mindestens 8 Zeichen",
"lowercase": "Enthält Kleinbuchstaben", "lowercase": "Enthält Kleinbuchstaben",
"uppercase": "Enthält Großbuchstaben", "uppercase": "Enthält Großbuchstaben",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Κωδικός", "label": "Κωδικός",
"requirement": { "suggestion": {
"length": "", "length": "",
"lowercase": "Περιλαμβάνει πεζό γράμμα", "lowercase": "Περιλαμβάνει πεζό γράμμα",
"uppercase": "Περιλαμβάνει κεφαλαίο γράμμα", "uppercase": "Περιλαμβάνει κεφαλαίο γράμμα",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Password", "label": "Password",
"requirement": { "suggestion": {
"length": "Includes a minimum of 8 characters", "length": "Includes a minimum of 8 characters",
"lowercase": "Includes a lower-case letter", "lowercase": "Includes a lower-case letter",
"uppercase": "Includes an upper-case letter", "uppercase": "Includes an upper-case letter",
+6 -2
View File
@@ -132,8 +132,9 @@
}, },
"password": { "password": {
"label": "Password", "label": "Password",
"requirement": { "suggestionsTitle": "Password suggestions",
"length": "Includes at least 8 characters", "suggestion": {
"length": "At least 8 characters",
"lowercase": "Includes lowercase letter", "lowercase": "Includes lowercase letter",
"uppercase": "Includes uppercase letter", "uppercase": "Includes uppercase letter",
"number": "Includes number", "number": "Includes number",
@@ -1405,6 +1406,9 @@
"openNewTab": { "openNewTab": {
"label": "Open in new tab" "label": "Open in new tab"
}, },
"withBorder": {
"label": "Show border"
},
"items": { "items": {
"label": "Bookmarks", "label": "Bookmarks",
"add": "Add bookmark" "add": "Add bookmark"
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Contraseña", "label": "Contraseña",
"requirement": { "suggestion": {
"length": "Incluye al menos 8 caracteres", "length": "Incluye al menos 8 caracteres",
"lowercase": "Incluye al menos una letra minúscula", "lowercase": "Incluye al menos una letra minúscula",
"uppercase": "Incluye al menos una letra mayúscula", "uppercase": "Incluye al menos una letra mayúscula",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "", "label": "",
"requirement": { "suggestion": {
"length": "", "length": "",
"lowercase": "", "lowercase": "",
"uppercase": "", "uppercase": "",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Mot de passe", "label": "Mot de passe",
"requirement": { "suggestion": {
"length": "Contient au moins 8 caractères", "length": "Contient au moins 8 caractères",
"lowercase": "Inclut une lettre minuscule", "lowercase": "Inclut une lettre minuscule",
"uppercase": "Inclut une lettre majuscule", "uppercase": "Inclut une lettre majuscule",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "סיסמה", "label": "סיסמה",
"requirement": { "suggestion": {
"length": "כולל לפחות 8 תווים", "length": "כולל לפחות 8 תווים",
"lowercase": "אפשר אותיות קטנות", "lowercase": "אפשר אותיות קטנות",
"uppercase": "אפשר אותיות גדולות", "uppercase": "אפשר אותיות גדולות",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Lozinka", "label": "Lozinka",
"requirement": { "suggestion": {
"length": "", "length": "",
"lowercase": "Uključuje mala slova", "lowercase": "Uključuje mala slova",
"uppercase": "Uključuje veliko slovo", "uppercase": "Uključuje veliko slovo",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Jelszó", "label": "Jelszó",
"requirement": { "suggestion": {
"length": "Tartalmazzon legalább 8 karaktert", "length": "Tartalmazzon legalább 8 karaktert",
"lowercase": "Tartalmazzon kisbetűt", "lowercase": "Tartalmazzon kisbetűt",
"uppercase": "Tartalmazzon nagybetűt", "uppercase": "Tartalmazzon nagybetűt",
+6 -6
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Password", "label": "Password",
"requirement": { "suggestion": {
"length": "Include almeno 8 caratteri", "length": "Include almeno 8 caratteri",
"lowercase": "Include lettera minuscola", "lowercase": "Include lettera minuscola",
"uppercase": "Include lettera maiuscola", "uppercase": "Include lettera maiuscola",
@@ -1056,7 +1056,7 @@
} }
}, },
"noResults": { "noResults": {
"title": "" "title": "Nessun media caricato"
} }
}, },
"common": { "common": {
@@ -1088,8 +1088,8 @@
"checkLogs": "Controlla i log per maggiori dettagli", "checkLogs": "Controlla i log per maggiori dettagli",
"tryAgain": "Riprova", "tryAgain": "Riprova",
"loading": "Caricamento", "loading": "Caricamento",
"show": "", "show": "Visualizza",
"hide": "" "hide": "Nascondi"
}, },
"here": "qui", "here": "qui",
"iconPicker": { "iconPicker": {
@@ -1255,7 +1255,7 @@
"label": { "label": {
"create": "Nuova categoria", "create": "Nuova categoria",
"changePosition": "Cambia posizione", "changePosition": "Cambia posizione",
"settings": "" "settings": "Impostazioni"
} }
}, },
"openAllInNewTabs": { "openAllInNewTabs": {
@@ -3459,7 +3459,7 @@
"private": "Questa board è privata" "private": "Questa board è privata"
}, },
"noResults": { "noResults": {
"title": "" "title": "Ancora nessuna board"
}, },
"modal": { "modal": {
"createBoard": { "createBoard": {
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "パスワード", "label": "パスワード",
"requirement": { "suggestion": {
"length": "少なくとも 8 文字以上を含む", "length": "少なくとも 8 文字以上を含む",
"lowercase": "小文字を含む", "lowercase": "小文字を含む",
"uppercase": "大文字を含む", "uppercase": "大文字を含む",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "비밀번호", "label": "비밀번호",
"requirement": { "suggestion": {
"length": "", "length": "",
"lowercase": "소문자 포함", "lowercase": "소문자 포함",
"uppercase": "대문자 포함", "uppercase": "대문자 포함",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Slaptažodis", "label": "Slaptažodis",
"requirement": { "suggestion": {
"length": "", "length": "",
"lowercase": "", "lowercase": "",
"uppercase": "", "uppercase": "",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Parole", "label": "Parole",
"requirement": { "suggestion": {
"length": "", "length": "",
"lowercase": "Ietver mazo burtu", "lowercase": "Ietver mazo burtu",
"uppercase": "Ietver lielo burtu", "uppercase": "Ietver lielo burtu",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Wachtwoord", "label": "Wachtwoord",
"requirement": { "suggestion": {
"length": "Bevat ten minste 8 tekens", "length": "Bevat ten minste 8 tekens",
"lowercase": "Inclusief kleine letter", "lowercase": "Inclusief kleine letter",
"uppercase": "Inclusief hoofdletter", "uppercase": "Inclusief hoofdletter",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Passord", "label": "Passord",
"requirement": { "suggestion": {
"length": "Inkluderer minst 8 tegn", "length": "Inkluderer minst 8 tegn",
"lowercase": "Inkluderer liten bokstav", "lowercase": "Inkluderer liten bokstav",
"uppercase": "Inkluderer stor bokstav", "uppercase": "Inkluderer stor bokstav",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Hasło", "label": "Hasło",
"requirement": { "suggestion": {
"length": "Zawiera co najmniej 8 znaków", "length": "Zawiera co najmniej 8 znaków",
"lowercase": "Zawiera małą literę", "lowercase": "Zawiera małą literę",
"uppercase": "Zawiera wielką literę", "uppercase": "Zawiera wielką literę",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Senha", "label": "Senha",
"requirement": { "suggestion": {
"length": "Inclua pelo menos 8 caracteres", "length": "Inclua pelo menos 8 caracteres",
"lowercase": "Inclui letras minúsculas", "lowercase": "Inclui letras minúsculas",
"uppercase": "Inclui letras maiúsculas", "uppercase": "Inclui letras maiúsculas",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "", "label": "",
"requirement": { "suggestion": {
"length": "", "length": "",
"lowercase": "", "lowercase": "",
"uppercase": "", "uppercase": "",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Parola", "label": "Parola",
"requirement": { "suggestion": {
"length": "", "length": "",
"lowercase": "Să includă litere mici", "lowercase": "Să includă litere mici",
"uppercase": "Să includă litere mari", "uppercase": "Să includă litere mari",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Пароль", "label": "Пароль",
"requirement": { "suggestion": {
"length": "Содержит не менее 8 символов", "length": "Содержит не менее 8 символов",
"lowercase": "Содержит строчную букву", "lowercase": "Содержит строчную букву",
"uppercase": "Содержит заглавную букву", "uppercase": "Содержит заглавную букву",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Heslo", "label": "Heslo",
"requirement": { "suggestion": {
"length": "Obsahuje aspoň 8 znakov", "length": "Obsahuje aspoň 8 znakov",
"lowercase": "Zahŕňa malé písmeno", "lowercase": "Zahŕňa malé písmeno",
"uppercase": "Zahŕňa veľké písmená", "uppercase": "Zahŕňa veľké písmená",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Geslo", "label": "Geslo",
"requirement": { "suggestion": {
"length": "", "length": "",
"lowercase": "Vključuje male črke", "lowercase": "Vključuje male črke",
"uppercase": "Vključuje velike tiskane črke", "uppercase": "Vključuje velike tiskane črke",
+107 -107
View File
@@ -82,7 +82,7 @@
"form": { "form": {
"name": { "name": {
"label": "Gruppens namn", "label": "Gruppens namn",
"description": "" "description": "Namnet måste matcha admin-grupp i extern tjänst"
} }
} }
}, },
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Lösenord", "label": "Lösenord",
"requirement": { "suggestion": {
"length": "Använd minst 8 tecken", "length": "Använd minst 8 tecken",
"lowercase": "Använd liten bokstav", "lowercase": "Använd liten bokstav",
"uppercase": "Använd stor bokstav", "uppercase": "Använd stor bokstav",
@@ -159,7 +159,7 @@
"label": "Visa sökresultat i ny flik" "label": "Visa sökresultat i ny flik"
}, },
"ddgBangs": { "ddgBangs": {
"label": "" "label": "Aktivera DuckDuckGo bangs"
} }
}, },
"error": { "error": {
@@ -252,10 +252,10 @@
"changeDdgBangs": { "changeDdgBangs": {
"notification": { "notification": {
"success": { "success": {
"message": "" "message": "DuckDuckGo bangs inställningen ändrad"
}, },
"error": { "error": {
"message": "" "message": "DuckDuckGo bangs inställningen kunde inte ändras"
} }
} }
}, },
@@ -350,12 +350,12 @@
"description": "Tillåt medlemmar att addera valfri applikation på sina tavlor" "description": "Tillåt medlemmar att addera valfri applikation på sina tavlor"
}, },
"modify-all": { "modify-all": {
"label": "", "label": "Modifiera alla appar",
"description": "" "description": "Tillåt att medlemmar modifierar alla appar"
}, },
"full-all": { "full-all": {
"label": "", "label": "Fullständiga rättigheter till appar",
"description": "" "description": "Tillåt att medlemmar hanterar, använder och raderar alla appar"
} }
} }
}, },
@@ -363,20 +363,20 @@
"title": "Tavlor", "title": "Tavlor",
"item": { "item": {
"create": { "create": {
"label": "", "label": "Skapa tavlor",
"description": "" "description": "Tillåt att medlemmar skapar tavlor"
}, },
"view-all": { "view-all": {
"label": "", "label": "Se alla tavlor",
"description": "" "description": "Tillåt att medlemmar ser alla tavlor"
}, },
"modify-all": { "modify-all": {
"label": "", "label": "Modifiera alla tavlor",
"description": "" "description": "Tillåt att medlemmar modifierar alla tavlor (Detta inkluderar inte åtkomstkontroll eller farozonen)"
}, },
"full-all": { "full-all": {
"label": "", "label": "Fullständiga rättigheter till tavlor",
"description": "" "description": "Tillåt att medlemmar ser, modifierar och tar bort alla tavlor (detta inkluderar åtkomstkontroll och farozonen)"
} }
} }
}, },
@@ -392,38 +392,38 @@
"description": "Tillåter medlemmar att addera valfri integration till sina tavlor" "description": "Tillåter medlemmar att addera valfri integration till sina tavlor"
}, },
"interact-all": { "interact-all": {
"label": "", "label": "Interagera med alla integrationer",
"description": "" "description": "Tillåt att medlemmar interagerar med alla integrationer"
}, },
"full-all": { "full-all": {
"label": "", "label": "Fullständiga rättigheter till integrationer",
"description": "" "description": "Tillåt att medlemmar hanterar, använder och interagerar med alla integrationer"
} }
} }
}, },
"media": { "media": {
"title": "", "title": "Media",
"item": { "item": {
"upload": { "upload": {
"label": "", "label": "Ladda upp media",
"description": "" "description": "Tillåt att medlemmar laddar upp media"
}, },
"view-all": { "view-all": {
"label": "", "label": "Se all media",
"description": "" "description": "Tillåt att medlemmar ser all media"
}, },
"full-all": { "full-all": {
"label": "", "label": "Fullständiga rättigheter till media",
"description": "" "description": "Tillåt att medlemmar hanterar och raderar all media"
} }
} }
}, },
"other": { "other": {
"title": "", "title": "Annat",
"item": { "item": {
"view-logs": { "view-logs": {
"label": "", "label": "Se loggar",
"description": "" "description": "Tillåt att medlemmar ser loggar"
} }
} }
}, },
@@ -435,32 +435,32 @@
"description": "Tillåt medlemmar att skapa sökmotorer" "description": "Tillåt medlemmar att skapa sökmotorer"
}, },
"modify-all": { "modify-all": {
"label": "", "label": "Modifiera alla sökmotorer",
"description": "" "description": "Tillåt att medlemmar modifierar alla sökmotorer"
}, },
"full-all": { "full-all": {
"label": "", "label": "Fullständiga rättigheter till sökmotorer",
"description": "" "description": "Tillåt att medlemmar hanterar och raderar alla sökmotorer"
} }
} }
} }
}, },
"memberNotice": { "memberNotice": {
"mixed": "", "mixed": "Vissa medlemmar är från externa tjänster och kan inte hantera här",
"external": "" "external": "Alla medlemmar är från externa tjänster och kan inte hanteras här"
}, },
"reservedNotice": { "reservedNotice": {
"message": "" "message": "Denna grupp är reserverad för systemet så vissa åtgärder är begränsade. <checkoutDocs></checkoutDocs>"
}, },
"action": { "action": {
"create": { "create": {
"label": "Addera en grupp", "label": "Addera en grupp",
"notification": { "notification": {
"success": { "success": {
"message": "" "message": "Gruppen har skapats"
}, },
"error": { "error": {
"message": "" "message": "Gruppen kunde inte skapas"
} }
} }
}, },
@@ -470,7 +470,7 @@
"confirm": "Är du säker på att du vill överföra ägarskapet för gruppen {name} till {username}?", "confirm": "Är du säker på att du vill överföra ägarskapet för gruppen {name} till {username}?",
"notification": { "notification": {
"success": { "success": {
"message": "" "message": "Överfört grupp {group} till {user}"
}, },
"error": { "error": {
"message": "Kunde inte överföra ägarskap" "message": "Kunde inte överföra ägarskap"
@@ -501,38 +501,38 @@
"notification": { "notification": {
"success": { "success": {
"title": "Behörigheter sparade", "title": "Behörigheter sparade",
"message": "" "message": "Rättigheterna sparade"
}, },
"error": { "error": {
"title": "", "title": "Rättigheterna ej sparade",
"message": "" "message": "Rättigheterna har inte sparats"
} }
} }
}, },
"update": { "update": {
"notification": { "notification": {
"success": { "success": {
"message": "" "message": "Gruppen {name} har sparats"
}, },
"error": { "error": {
"message": "" "message": "Kunde inte spara gruppen {name}"
} }
} }
}, },
"select": { "select": {
"label": "Välj en grupp", "label": "Välj en grupp",
"notFound": "" "notFound": "Ingen grupp hittad"
}, },
"settings": { "settings": {
"board": { "board": {
"notification": { "notification": {
"success": { "success": {
"title": "Inställningarna sparades", "title": "Inställningarna sparades",
"message": "" "message": "Inställningarna för tavlan har sparats"
}, },
"error": { "error": {
"title": "Kunde inte spara inställningarna", "title": "Kunde inte spara inställningarna",
"message": "" "message": "Kunde inte spara inställningarna för tavlan"
} }
} }
} }
@@ -540,10 +540,10 @@
"changePosition": { "changePosition": {
"notification": { "notification": {
"success": { "success": {
"message": "" "message": "Ordning ändrad"
}, },
"error": { "error": {
"message": "" "message": "Ordningen kunde ändras"
} }
} }
} }
@@ -559,20 +559,20 @@
"list": { "list": {
"title": "Applikationer", "title": "Applikationer",
"noResults": { "noResults": {
"title": "", "title": "Det finns inga appar ännu",
"action": "" "action": "Skapa din första app"
} }
}, },
"create": { "create": {
"title": "Addera applikation", "title": "Addera applikation",
"notification": { "notification": {
"success": { "success": {
"title": "", "title": "Skapandet lyckades",
"message": "" "message": "Appen skapades framgångsrikt"
}, },
"error": { "error": {
"title": "", "title": "Skapandet misslyckades",
"message": "" "message": "Appen kunde inte skapas"
} }
} }
}, },
@@ -581,11 +581,11 @@
"notification": { "notification": {
"success": { "success": {
"title": "Ändringarna har sparats", "title": "Ändringarna har sparats",
"message": "" "message": "Appen sparades"
}, },
"error": { "error": {
"title": "Misslyckades med att spara ändringarna", "title": "Misslyckades med att spara ändringarna",
"message": "" "message": "Appen kunde inte sparas"
} }
} }
}, },
@@ -594,12 +594,12 @@
"message": "Är du säker på att du vill ta bort applikationen {name}?", "message": "Är du säker på att du vill ta bort applikationen {name}?",
"notification": { "notification": {
"success": { "success": {
"title": "", "title": "Borttagning lyckades",
"message": "" "message": "Appen har tagits bort"
}, },
"error": { "error": {
"title": "", "title": "Borttagning misslyckades",
"message": "" "message": "Kunde inte ta bort appen"
} }
} }
} }
@@ -612,21 +612,21 @@
"label": "Beskrivning" "label": "Beskrivning"
}, },
"url": { "url": {
"label": "" "label": "URL"
}, },
"useDifferentUrlForPing": { "useDifferentUrlForPing": {
"checkbox": { "checkbox": {
"label": "", "label": "Använd annan URL för ping",
"description": "" "description": "Användbart om Homarr kan komma åt appen direkt via ett internt host-namn eller nätverk för att undvika bandbreddsanvändning hos internetleverantör"
} }
} }
}, },
"action": { "action": {
"select": { "select": {
"label": "", "label": "Välj app",
"notFound": "", "notFound": "Ingen app hittades",
"search": "Sök efter en applikation", "search": "Sök efter en applikation",
"noResults": "", "noResults": "Inga resultat",
"action": "Välj {app}", "action": "Välj {app}",
"title": "Välj en applikation att lägga till på tavlan" "title": "Välj en applikation att lägga till på tavlan"
}, },
@@ -642,56 +642,56 @@
"page": { "page": {
"list": { "list": {
"title": "Integrationer", "title": "Integrationer",
"search": "", "search": "Sök integrationer",
"noResults": { "noResults": {
"title": "" "title": "Det finns inga integrationer ännu"
} }
}, },
"create": { "create": {
"title": "", "title": "Ny {name} integration",
"notification": { "notification": {
"success": { "success": {
"title": "", "title": "Skapandet lyckades",
"message": "" "message": "Integrationen har skapats"
}, },
"error": { "error": {
"title": "", "title": "Skapandet misslyckades",
"message": "" "message": "Integrationen kunde inte skapas"
} }
}, },
"app": { "app": {
"option": { "option": {
"existing": { "existing": {
"title": "", "title": "Befintlig",
"label": "" "label": "Välj befintlig app"
}, },
"new": { "new": {
"title": "", "title": "Ny",
"url": { "url": {
"label": "", "label": "App URL",
"description": "" "description": "URL:en som appen kommer att öppna när den används från tavlan"
} }
} }
} }
} }
}, },
"edit": { "edit": {
"title": "", "title": "Redigera {name} integration",
"notification": { "notification": {
"success": { "success": {
"title": "Ändringarna har sparats", "title": "Ändringarna har sparats",
"message": "" "message": "Integrationen sparad"
}, },
"error": { "error": {
"title": "", "title": "Kunde inte tillämpa ändringar",
"message": "" "message": "Integrationen kunde inte sparas"
} }
}, },
"app": { "app": {
"action": { "action": {
"add": "", "add": "Länka en app",
"remove": "", "remove": "Ta bort länk",
"select": "" "select": "Välj en app att länka till"
} }
} }
}, },
@@ -700,12 +700,12 @@
"message": "Är du säker på att du vill ta bort integrationen {name}?", "message": "Är du säker på att du vill ta bort integrationen {name}?",
"notification": { "notification": {
"success": { "success": {
"title": "", "title": "Borttagning lyckades",
"message": "" "message": "Integrationen har tagits bort"
}, },
"error": { "error": {
"title": "", "title": "Borttagning misslyckades",
"message": "" "message": "Kunde inte ta bort integrationen"
} }
} }
} }
@@ -715,44 +715,44 @@
"label": "Namn" "label": "Namn"
}, },
"url": { "url": {
"label": "" "label": "URL"
}, },
"attemptSearchEngineCreation": { "attemptSearchEngineCreation": {
"label": "", "label": "Skapa sökmotor",
"description": "" "description": "Integrationen \"{kind}\" kan inte användas med sökmotorer. Markera detta för att automatiskt konfigurera sökmotorn."
}, },
"app": { "app": {
"sectionTitle": "" "sectionTitle": "Länkad app"
}, },
"createApp": { "createApp": {
"label": "", "label": "Skapa app",
"description": "" "description": "Skapa en app med samma namn och ikon som integrationen. Lämna fältet nedan tomt för att skapa en app med integrationens URL."
}, },
"appHref": { "appHref": {
"placeholder": "" "placeholder": "Egen app-URL"
} }
}, },
"action": { "action": {
"create": "" "create": "Ny integration"
}, },
"testConnection": { "testConnection": {
"action": { "action": {
"create": "", "create": "Testa anslutningen och skapa",
"edit": "" "edit": "Testa anslutningen och skapa"
}, },
"error": { "error": {
"common": { "common": {
"cause": { "cause": {
"title": "" "title": "Orsak med mer detaljer"
} }
}, },
"unknown": { "unknown": {
"title": "", "title": "Okänt fel",
"description": "" "description": "Ett okänt fel inträffade, öppna orsaken nedan för att se mer detaljer"
}, },
"parse": { "parse": {
"title": "", "title": "Parsningsfel",
"description": "" "description": "Svaret kunde inte analyseras. Verifiera att URL pekar på bas-URL för tjänsten."
}, },
"authorization": { "authorization": {
"title": "", "title": "",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Şifre", "label": "Şifre",
"requirement": { "suggestion": {
"length": "En az 8 karakter içermeli", "length": "En az 8 karakter içermeli",
"lowercase": "Küçük harf içermeli", "lowercase": "Küçük harf içermeli",
"uppercase": "Büyük harf içermeli", "uppercase": "Büyük harf içermeli",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Пароль", "label": "Пароль",
"requirement": { "suggestion": {
"length": "Включає принаймні 8 символів", "length": "Включає принаймні 8 символів",
"lowercase": "Включає малу літеру", "lowercase": "Включає малу літеру",
"uppercase": "Включає велику літеру", "uppercase": "Включає велику літеру",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Mật khẩu", "label": "Mật khẩu",
"requirement": { "suggestion": {
"length": "Bao gồm ít nhất 8 kí tự", "length": "Bao gồm ít nhất 8 kí tự",
"lowercase": "Bao gồm chữ thường", "lowercase": "Bao gồm chữ thường",
"uppercase": "Bao gồm chữ in hoa", "uppercase": "Bao gồm chữ in hoa",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "密碼", "label": "密碼",
"requirement": { "suggestion": {
"length": "至少包含 8 個字符", "length": "至少包含 8 個字符",
"lowercase": "包含小寫字母", "lowercase": "包含小寫字母",
"uppercase": "包含大寫字母", "uppercase": "包含大寫字母",
@@ -20,15 +20,15 @@ export const CustomPasswordInput = ({ withPasswordRequirements, ...props }: Cust
}; };
const WithPasswordRequirements = (props: PasswordInputProps) => { const WithPasswordRequirements = (props: PasswordInputProps) => {
const [value, setValue] = useState(""); const [password, setPassword] = useState("");
const onChange = (event: ChangeEvent<HTMLInputElement>) => { const onChange = (event: ChangeEvent<HTMLInputElement>) => {
setValue(event.currentTarget.value); setPassword(event.currentTarget.value);
props.onChange?.(event); props.onChange?.(event);
}; };
return ( return (
<PasswordRequirementsPopover password={value}> <PasswordRequirementsPopover password={password}>
<PasswordInput {...props} onChange={onChange} /> <PasswordInput {...props} onChange={onChange} />
</PasswordRequirementsPopover> </PasswordRequirementsPopover>
); );
@@ -1,14 +1,19 @@
import { rem, Text } from "@mantine/core"; import { rem, Text } from "@mantine/core";
import { IconCheck, IconX } from "@tabler/icons-react"; import { IconCheck, IconPoint } from "@tabler/icons-react";
const getRequirementDisplay = (meets: boolean) =>
({
true: { color: "teal", Icon: IconCheck },
false: { color: "dimmed", Icon: IconPoint },
})[String(meets) as "true" | "false"];
export function PasswordRequirement({ meets, label }: { meets: boolean; label: string }) { export function PasswordRequirement({ meets, label }: { meets: boolean; label: string }) {
const display = getRequirementDisplay(meets);
const Icon = display.Icon;
return ( return (
<Text c={meets ? "teal" : "red"} display="flex" style={{ alignItems: "center" }} size="sm"> <Text c={display.color} display="flex" style={{ alignItems: "center" }} size="sm">
{meets ? ( <Icon style={{ width: rem(14), height: rem(14) }} />
<IconCheck style={{ width: rem(14), height: rem(14) }} />
) : (
<IconX style={{ width: rem(14), height: rem(14) }} />
)}
<Text span ml={10}> <Text span ml={10}>
{label} {label}
</Text> </Text>
@@ -1,16 +1,24 @@
import type { PropsWithChildren } from "react"; import type { PropsWithChildren } from "react";
import { useState } from "react"; import { useState } from "react";
import { Popover, Progress } from "@mantine/core"; import { Popover, Progress, Text } from "@mantine/core";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import { passwordRequirements } from "@homarr/validation/user"; import { passwordRequirements } from "@homarr/validation/user";
import { PasswordRequirement } from "./password-requirement"; import { PasswordRequirement } from "./password-requirement";
const strengthColorMap = [
{ min: 100, color: "teal" },
{ min: 51, color: "yellow" },
{ min: 0, color: "dimmed" },
] as const;
export const PasswordRequirementsPopover = ({ password, children }: PropsWithChildren<{ password: string }>) => { export const PasswordRequirementsPopover = ({ password, children }: PropsWithChildren<{ password: string }>) => {
const tPassword = useScopedI18n("user.field.password");
const requirements = useRequirements(); const requirements = useRequirements();
const strength = useStrength(password); const strength = useStrength(password);
const [popoverOpened, setPopoverOpened] = useState(false); const [popoverOpened, setPopoverOpened] = useState(false);
const strengthColor = strengthColorMap.find(({ min }) => strength >= min)?.color ?? "dimmed";
const checks = ( const checks = (
<> <>
{requirements.map((requirement) => ( {requirements.map((requirement) => (
@@ -19,8 +27,6 @@ export const PasswordRequirementsPopover = ({ password, children }: PropsWithChi
</> </>
); );
const color = strength === 100 ? "teal" : strength > 50 ? "yellow" : "red";
return ( return (
<Popover opened={popoverOpened} position="bottom" width="target" transitionProps={{ transition: "pop" }}> <Popover opened={popoverOpened} position="bottom" width="target" transitionProps={{ transition: "pop" }}>
<Popover.Target> <Popover.Target>
@@ -29,7 +35,10 @@ export const PasswordRequirementsPopover = ({ password, children }: PropsWithChi
</div> </div>
</Popover.Target> </Popover.Target>
<Popover.Dropdown> <Popover.Dropdown>
<Progress color={color} value={strength} size={5} mb="xs" /> <Progress color={strengthColor} value={strength} size={5} mb="xs" />
<Text size="sm" c="dimmed" mb="xs">
{tPassword("suggestionsTitle")}
</Text>
{checks} {checks}
</Popover.Dropdown> </Popover.Dropdown>
</Popover> </Popover>
@@ -37,7 +46,7 @@ export const PasswordRequirementsPopover = ({ password, children }: PropsWithChi
}; };
const useRequirements = () => { const useRequirements = () => {
const t = useScopedI18n("user.field.password.requirement"); const t = useScopedI18n("user.field.password.suggestion");
return passwordRequirements.map(({ check, value }) => ({ check, label: t(value) })); return passwordRequirements.map(({ check, value }) => ({ check, label: t(value) }));
}; };
+2 -16
View File
@@ -19,24 +19,10 @@ export const passwordRequirements = [
{ check: regexCheck(/[$&+,:;=?@#|'<>.^*()%!\-~`"_/\\[\]{}]/), value: "special" }, { check: regexCheck(/[$&+,:;=?@#|'<>.^*()%!\-~`"_/\\[\]{}]/), value: "special" },
] satisfies { ] satisfies {
check: (value: string) => boolean; check: (value: string) => boolean;
value: keyof TranslationObject["user"]["field"]["password"]["requirement"]; value: keyof TranslationObject["user"]["field"]["password"]["suggestion"];
}[]; }[];
export const userPasswordSchema = z export const userPasswordSchema = z.string().min(8).max(255);
.string()
.min(8)
.max(255)
.refine(
(value) => {
return passwordRequirements.every((requirement) => requirement.check(value));
},
{
params: createCustomErrorParams({
key: "passwordRequirements",
params: {},
}),
},
);
const addConfirmPasswordRefinement = < const addConfirmPasswordRefinement = <
TSchema extends z.ZodObject<{ password: z.core.$ZodString; confirmPassword: z.core.$ZodString }, z.core.$strip>, TSchema extends z.ZodObject<{ password: z.core.$ZodString; confirmPassword: z.core.$ZodString }, z.core.$strip>,
+17 -2
View File
@@ -16,7 +16,7 @@ export default function BookmarksWidget({ options, itemId }: WidgetComponentProp
const board = useRequiredBoard(); const board = useRequiredBoard();
const [data] = clientApi.app.byIds.useSuspenseQuery(options.items, { const [data] = clientApi.app.byIds.useSuspenseQuery(options.items, {
select(data) { select(data) {
return data.sort((appA, appB) => options.items.indexOf(appA.id) - options.items.indexOf(appB.id)); return data.toSorted((appA, appB) => options.items.indexOf(appA.id) - options.items.indexOf(appB.id));
}, },
}); });
@@ -56,6 +56,7 @@ export default function BookmarksWidget({ options, itemId }: WidgetComponentProp
hideIcon={options.hideIcon} hideIcon={options.hideIcon}
hideHostname={options.hideHostname} hideHostname={options.hideHostname}
openNewTab={options.openNewTab} openNewTab={options.openNewTab}
withBorder={options.withBorder}
hasIconColor={board.iconColor !== null} hasIconColor={board.iconColor !== null}
/> />
)} )}
@@ -67,6 +68,7 @@ export default function BookmarksWidget({ options, itemId }: WidgetComponentProp
hideIcon={options.hideIcon} hideIcon={options.hideIcon}
hideHostname={options.hideHostname} hideHostname={options.hideHostname}
openNewTab={options.openNewTab} openNewTab={options.openNewTab}
withBorder={options.withBorder}
hasIconColor={board.iconColor !== null} hasIconColor={board.iconColor !== null}
/> />
)} )}
@@ -81,6 +83,7 @@ interface FlexLayoutProps {
hideIcon: boolean; hideIcon: boolean;
hideHostname: boolean; hideHostname: boolean;
openNewTab: boolean; openNewTab: boolean;
withBorder: boolean;
hasIconColor: boolean; hasIconColor: boolean;
} }
@@ -91,6 +94,7 @@ const FlexLayout = ({
hideIcon, hideIcon,
hideHostname, hideHostname,
openNewTab, openNewTab,
withBorder,
hasIconColor, hasIconColor,
}: FlexLayoutProps) => { }: FlexLayoutProps) => {
const board = useRequiredBoard(); const board = useRequiredBoard();
@@ -106,7 +110,15 @@ const FlexLayout = ({
key={app.id} key={app.id}
w="100%" w="100%"
> >
<Card radius={board.itemRadius} className={classes.card} w="100%" display="flex" p={4} h="100%"> <Card
radius={board.itemRadius}
className={classes.card}
w="100%"
display="flex"
p={4}
h="100%"
withBorder={withBorder}
>
{direction === "row" ? ( {direction === "row" ? (
<VerticalItem <VerticalItem
app={app} app={app}
@@ -138,6 +150,7 @@ interface GridLayoutProps {
hideIcon: boolean; hideIcon: boolean;
hideHostname: boolean; hideHostname: boolean;
openNewTab: boolean; openNewTab: boolean;
withBorder: boolean;
itemDirection: "horizontal" | "vertical"; itemDirection: "horizontal" | "vertical";
hasIconColor: boolean; hasIconColor: boolean;
} }
@@ -148,6 +161,7 @@ const GridLayout = ({
hideIcon, hideIcon,
hideHostname, hideHostname,
openNewTab, openNewTab,
withBorder,
itemDirection, itemDirection,
hasIconColor, hasIconColor,
}: GridLayoutProps) => { }: GridLayoutProps) => {
@@ -168,6 +182,7 @@ const GridLayout = ({
h="100%" h="100%"
className={combineClasses(classes.card, classes["card-grid"])} className={combineClasses(classes.card, classes["card-grid"])}
radius={board.itemRadius} radius={board.itemRadius}
withBorder={withBorder}
p="xs" p="xs"
> >
{itemDirection === "horizontal" ? ( {itemDirection === "horizontal" ? (
+1
View File
@@ -24,6 +24,7 @@ export const { definition, componentLoader } = createWidgetDefinition("bookmarks
hideIcon: factory.switch({ defaultValue: false }), hideIcon: factory.switch({ defaultValue: false }),
hideHostname: factory.switch({ defaultValue: false }), hideHostname: factory.switch({ defaultValue: false }),
openNewTab: factory.switch({ defaultValue: true }), openNewTab: factory.switch({ defaultValue: true }),
withBorder: factory.switch({ defaultValue: false }),
items: factory.sortableItemList<RouterOutputs["app"]["all"][number], string>({ items: factory.sortableItemList<RouterOutputs["app"]["all"][number], string>({
ItemComponent: ({ item, handle: Handle, removeItem, rootAttributes }) => { ItemComponent: ({ item, handle: Handle, removeItem, rootAttributes }) => {
return ( return (
@@ -1,20 +1,10 @@
.statItem { .statItem {
padding: 12px; padding: 12px;
border-radius: var(--mantine-radius-md); border-radius: var(--mantine-radius-md);
background-color: var(--mantine-color-gray-0); background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
transition: all 0.2s ease; transition: all 0.2s ease;
&:hover { &:hover {
background-color: var(--mantine-color-gray-1); background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
}
}
@media (prefers-color-scheme: dark) {
.statItem {
background-color: var(--mantine-color-dark-6);
&:hover {
background-color: var(--mantine-color-dark-5);
}
} }
} }
+798 -830
View File
File diff suppressed because it is too large. Load diff
+46 -46
View File
@@ -6,7 +6,7 @@ catalog:
"@auth/core": ^0.41.2 "@auth/core": ^0.41.2
"@auth/drizzle-adapter": ^1.11.2 "@auth/drizzle-adapter": ^1.11.2
"@ctrl/deluge": ^8.0.0 "@ctrl/deluge": ^8.0.0
"@ctrl/qbittorrent": ^9.12.1 "@ctrl/qbittorrent": ^9.12.2
"@ctrl/transmission": ^7.4.0 "@ctrl/transmission": ^7.4.0
"@dnd-kit/core": ^6.3.1 "@dnd-kit/core": ^6.3.1
"@dnd-kit/modifiers": ^9.0.0 "@dnd-kit/modifiers": ^9.0.0
@@ -20,17 +20,17 @@ catalog:
"@immich/sdk": ^2.7.5 "@immich/sdk": ^2.7.5
"@jellyfin/sdk": ^0.13.0 "@jellyfin/sdk": ^0.13.0
"@kubernetes/client-node": ^1.4.0 "@kubernetes/client-node": ^1.4.0
"@mantine/charts": ^9.2.1 "@mantine/charts": ^9.2.2
"@mantine/colors-generator": ^9.2.1 "@mantine/colors-generator": ^9.2.2
"@mantine/core": ^9.2.1 "@mantine/core": ^9.2.2
"@mantine/dates": ^9.2.1 "@mantine/dates": ^9.2.2
"@mantine/dropzone": ^9.2.1 "@mantine/dropzone": ^9.2.2
"@mantine/form": ^9.2.1 "@mantine/form": ^9.2.2
"@mantine/hooks": ^9.2.1 "@mantine/hooks": ^9.2.2
"@mantine/modals": ^9.2.1 "@mantine/modals": ^9.2.2
"@mantine/notifications": ^9.2.1 "@mantine/notifications": ^9.2.2
"@mantine/spotlight": ^9.2.1 "@mantine/spotlight": ^9.2.2
"@mantine/tiptap": ^9.2.1 "@mantine/tiptap": ^9.2.2
"@next/eslint-plugin-next": 16.2.6 "@next/eslint-plugin-next": 16.2.6
"@octokit/auth-app": ^8.2.0 "@octokit/auth-app": ^8.2.0
"@paralleldrive/cuid2": ^3.3.0 "@paralleldrive/cuid2": ^3.3.0
@@ -43,34 +43,34 @@ catalog:
"@semantic-release/release-notes-generator": ^14.1.1 "@semantic-release/release-notes-generator": ^14.1.1
"@t3-oss/env-nextjs": ^0.13.11 "@t3-oss/env-nextjs": ^0.13.11
"@tabler/icons-react": ^3.44.0 "@tabler/icons-react": ^3.44.0
"@tanstack/react-query": ^5.100.13 "@tanstack/react-query": ^5.100.14
"@tanstack/react-query-devtools": ^5.100.13 "@tanstack/react-query-devtools": ^5.100.14
"@tanstack/react-query-next-experimental": ^5.100.13 "@tanstack/react-query-next-experimental": ^5.100.14
"@testcontainers/mysql": ^12.0.0 "@testcontainers/mysql": ^12.0.1
"@testcontainers/postgresql": ^12.0.0 "@testcontainers/postgresql": ^12.0.1
"@testcontainers/redis": ^12.0.0 "@testcontainers/redis": ^12.0.1
"@tiptap/extension-color": 3.23.6 "@tiptap/extension-color": 3.24.0
"@tiptap/extension-highlight": 3.23.6 "@tiptap/extension-highlight": 3.24.0
"@tiptap/extension-image": 3.23.6 "@tiptap/extension-image": 3.24.0
"@tiptap/extension-link": ^3.23.6 "@tiptap/extension-link": ^3.24.0
"@tiptap/extension-placeholder": ^3.23.6 "@tiptap/extension-placeholder": ^3.24.0
"@tiptap/extension-table": 3.23.6 "@tiptap/extension-table": 3.24.0
"@tiptap/extension-table-cell": 3.23.6 "@tiptap/extension-table-cell": 3.24.0
"@tiptap/extension-table-header": 3.23.6 "@tiptap/extension-table-header": 3.24.0
"@tiptap/extension-table-row": 3.23.6 "@tiptap/extension-table-row": 3.24.0
"@tiptap/extension-task-item": 3.23.6 "@tiptap/extension-task-item": 3.24.0
"@tiptap/extension-task-list": 3.23.6 "@tiptap/extension-task-list": 3.24.0
"@tiptap/extension-text-align": 3.23.6 "@tiptap/extension-text-align": 3.24.0
"@tiptap/extension-text-style": 3.23.6 "@tiptap/extension-text-style": 3.24.0
"@tiptap/extension-underline": 3.23.6 "@tiptap/extension-underline": 3.24.0
"@tiptap/react": ^3.23.6 "@tiptap/react": ^3.24.0
"@tiptap/starter-kit": ^3.23.6 "@tiptap/starter-kit": ^3.24.0
"@trpc/client": ^11.17.0 "@trpc/client": ^11.17.0
"@trpc/next": ^11.17.0 "@trpc/next": ^11.17.0
"@trpc/react-query": ^11.17.0 "@trpc/react-query": ^11.17.0
"@trpc/server": ^11.17.0 "@trpc/server": ^11.17.0
"@trpc/tanstack-react-query": ^11.17.0 "@trpc/tanstack-react-query": ^11.17.0
"@turbo/gen": ^2.9.14 "@turbo/gen": ^2.9.16
"@types/adm-zip": 0.5.8 "@types/adm-zip": 0.5.8
"@types/bcrypt": 6.0.0 "@types/bcrypt": 6.0.0
"@types/better-sqlite3": 7.6.13 "@types/better-sqlite3": 7.6.13
@@ -109,7 +109,7 @@ catalog:
croner: 10.0.1 croner: 10.0.1
cross-env: ^10.1.0 cross-env: ^10.1.0
crypto-js: ^4.2.0 crypto-js: ^4.2.0
dayjs: ^1.11.20 dayjs: ^1.11.21
deepmerge: 4.3.1 deepmerge: 4.3.1
dns-caching: ^0.2.9 dns-caching: ^0.2.9
dockerode: ^5.0.0 dockerode: ^5.0.0
@@ -124,30 +124,30 @@ catalog:
flag-icons: ^7.5.0 flag-icons: ^7.5.0
glob: ^13.0.6 glob: ^13.0.6
ical.js: ^2.2.1 ical.js: ^2.2.1
ioredis: 5.10.1 ioredis: 5.11.0
isomorphic-dompurify: ^3.14.0 isomorphic-dompurify: ^3.15.0
jotai: ^2.20.0 jotai: ^2.20.0
jsdom: ^29.1.1 jsdom: ^29.1.1
json5: ^2.2.3 json5: ^2.2.3
ldapts: 8.1.8 ldapts: 8.1.8
lodash.clonedeep: ^4.5.0 lodash.clonedeep: ^4.5.0
mantine-react-table: 2.0.0-beta.9 mantine-react-table: npm:mantine-react-table-open@9.0.3
maria2: ^0.4.1 maria2: ^0.4.1
mysql2: 3.22.3 mysql2: 3.22.4
next: 16.2.6 next: 16.2.6
next-auth: 5.0.0-beta.31 next-auth: 5.0.0-beta.31
next-intl: 4.12.0 next-intl: 4.13.0
node-loader: ^2.1.0 node-loader: ^2.1.0
octokit: ^5.0.5 octokit: ^5.0.5
oxfmt: ^0.51.0 oxfmt: ^0.52.0
oxlint: ^1.66.0 oxlint: ^1.67.0
pg: ^8.21.0 pg: ^8.21.0
postcss-preset-mantine: ^1.18.0 postcss-preset-mantine: ^1.18.0
prismjs: ^1.30.0 prismjs: ^1.30.0
proxmox-api: 1.1.1 proxmox-api: 1.1.1
react: 19.2.6 react: 19.2.6
react-dom: 19.2.6 react-dom: 19.2.6
react-error-boundary: ^6.1.1 react-error-boundary: ^6.1.2
react-markdown: ^10.1.0 react-markdown: ^10.1.0
react-quill-new: ^3.8.3 react-quill-new: ^3.8.3
react-simple-code-editor: ^0.14.1 react-simple-code-editor: ^0.14.1
@@ -158,12 +158,12 @@ catalog:
superjson: 2.2.6 superjson: 2.2.6
svgson: ^5.3.1 svgson: ^5.3.1
swagger-ui-react: ^5.32.6 swagger-ui-react: ^5.32.6
testcontainers: ^12.0.0 testcontainers: ^12.0.1
timezones-ical-library: ^2.2.0 timezones-ical-library: ^2.2.0
trpc-to-openapi: ^3.3.0 trpc-to-openapi: ^3.3.0
tsdav: ^2.2.2 tsdav: ^2.2.2
tsx: 4.20.4 tsx: 4.20.4
turbo: ^2.9.14 turbo: ^2.9.16
typescript: ^5.9.3 typescript: ^5.9.3
undici: 7.24.6 undici: 7.24.6
use-deep-compare-effect: ^1.8.1 use-deep-compare-effect: ^1.8.1