Compare commits

..
Author SHA1 Message Date
Releases Homarr ff1e4c1381 chore(release): 1.63.0 [skip ci] 2026-05-29 21:07:15 +00:00
homarr-releases[bot] 21bb20ce0c chore(release): automatic release v1.63.0 2026-05-29 21:06:26 +00:00
ThomasandMeier Lukas 2991777b0a feat(users): rework general settings (#4915)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2026-05-29 21:22:55 +02:00
ThomasandMeier Lukas 6cc2ac56a9 fix: prevent GitHub releases check from blocking app on API failure (#5807)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2026-05-29 21:14:51 +02:00
homarr-update-contributors[bot] 92086e060f chore: update contributors (#5818) 2026-05-29 20:52:54 +02:00
homarr-renovate[bot] 2aa5ab95e6 chore(deps): update dependency trpc-to-openapi to ^3.3.0 (#5802)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-29 11:44:26 +00:00
Meier Lukas 650f761325 ci(format): fails to apply format due to conflict between --write and --check (#5815) 2026-05-29 11:07:48 +02:00
David be61602576 feat(auth): support custom token endpoint auth methods (#5806) 2026-05-29 10:51:28 +02:00
homarr-crowdin[bot] d01834d975 chore(lang): updated translations from crowdin (#5814) 2026-05-29 10:07:48 +02:00
Thomas Camlong 4a67bf0ef3 fix(ui): use imported theme instead of undefined createTheme
createTheme was called without being imported, causing a runtime
ReferenceError on every page load. Use the already-imported theme
object from @homarr/ui which contains the full Mantine configuration.
2026-05-28 15:54:37 +02:00
Thomas 4454e3fb7a fix: matine ui fixes for concistency (#5733) 2026-05-28 10:59:03 +02:00
Releases Homarr 92b75cb924 chore(release): 1.62.0 [skip ci] 2026-05-22 20:48:36 +00:00
Meier Lukas 1ea008b71c chore(release): automatic release v1.62.0 2026-05-22 22:47:05 +02:00
99 changed files with 952 additions and 633 deletions

No files matched your search

+40
View File
@@ -1,3 +1,43 @@
## [1.63.0](https://github.com/homarr-labs/homarr/compare/v1.62.0...v1.63.0) (2026-05-29)
### Features
* **auth:** support custom token endpoint auth methods ([#5806](https://github.com/homarr-labs/homarr/issues/5806)) ([be61602](https://github.com/homarr-labs/homarr/commit/be61602576a8bec4bb7e4928d36e0854e926264d))
* auto-open widget settings on add with all integrations pre-selected ([#5767](https://github.com/homarr-labs/homarr/issues/5767)) ([f10bb3a](https://github.com/homarr-labs/homarr/commit/f10bb3ae23fefc9779332733706aa567cc40ba48))
* **board:** persist category collapse state in localStorage for guests ([#5714](https://github.com/homarr-labs/homarr/issues/5714)) ([66212b4](https://github.com/homarr-labs/homarr/commit/66212b4e475738e61d2ea78f3a7e72d7a610bf12)), closes [#3355](https://github.com/homarr-labs/homarr/issues/3355)
* **manage:** unified management page layout ([#5758](https://github.com/homarr-labs/homarr/issues/5758)) ([c252b4f](https://github.com/homarr-labs/homarr/commit/c252b4f9accc4523f222354c73f687714640701f))
* **users:** rework general settings ([#4915](https://github.com/homarr-labs/homarr/issues/4915)) ([2991777](https://github.com/homarr-labs/homarr/commit/2991777b0a0ae72522d0769a86a91250d618dbe6))
### Bug Fixes
* matine ui fixes for concistency ([#5733](https://github.com/homarr-labs/homarr/issues/5733)) ([4454e3f](https://github.com/homarr-labs/homarr/commit/4454e3fb7afa624ee2f1eaec015f2725e664ac46))
* parse Speedtest Tracker created_at as local time ([#5751](https://github.com/homarr-labs/homarr/issues/5751)) ([b19549b](https://github.com/homarr-labs/homarr/commit/b19549b601636bcbc2fadaa270a77a9fdb48b77a))
* prevent GitHub releases check from blocking app on API failure ([#5807](https://github.com/homarr-labs/homarr/issues/5807)) ([6cc2ac5](https://github.com/homarr-labs/homarr/commit/6cc2ac56a920563c47aaaab1b33b19a9b6d5ecee))
* **ui:** use imported theme instead of undefined createTheme ([4a67bf0](https://github.com/homarr-labs/homarr/commit/4a67bf0ef30ff8d10b06d2d3719ccab792395669))
### Performance Improvements
* **boards:** parallelize board page data fetching ([#5736](https://github.com/homarr-labs/homarr/issues/5736)) ([8f03a38](https://github.com/homarr-labs/homarr/commit/8f03a38de5b9b385da7ffcc9a963be00710acb5e))
## [1.62.0](https://github.com/homarr-labs/homarr/compare/v1.61.1...v1.62.0) (2026-05-22)
### Features
* **seed:** add demo mode with pre-seeded user ([#5739](https://github.com/homarr-labs/homarr/issues/5739)) ([dade477](https://github.com/homarr-labs/homarr/commit/dade477b3226765eb614e66588ebee84c45cb688))
### Bug Fixes
* **deps:** update dependency better-sqlite3 to ^12.10.0 ([#5727](https://github.com/homarr-labs/homarr/issues/5727)) ([044ad9e](https://github.com/homarr-labs/homarr/commit/044ad9edf9c1dc9b50b9591dea0b5f0cce4be01a))
* **docker:** improve icon matching ([#5693](https://github.com/homarr-labs/homarr/issues/5693)) ([7cd30b6](https://github.com/homarr-labs/homarr/commit/7cd30b640cbf47e4678abda18f83c0924f039b1b))
* exclude .env files from Docker build context ([#5738](https://github.com/homarr-labs/homarr/issues/5738)) ([fa73763](https://github.com/homarr-labs/homarr/commit/fa737638a812372d904207fe7af5bd7c6f53d159))
* **tracearr:** support absolute image urls ([#5654](https://github.com/homarr-labs/homarr/issues/5654)) ([7a12721](https://github.com/homarr-labs/homarr/commit/7a1272146c597c1f9df89d74b8389f70e7cdd61a))
### Performance Improvements
* (next branch) reduce idle memory on startup ([#5637](https://github.com/homarr-labs/homarr/issues/5637)) ([5ee84f8](https://github.com/homarr-labs/homarr/commit/5ee84f8cf464c308729281cb4db572950ba85a82))
* **boards:** remove loading overlay and show board immediately ([#5735](https://github.com/homarr-labs/homarr/issues/5735)) ([df4b57b](https://github.com/homarr-labs/homarr/commit/df4b57bd414d4402522f9210b1470d0561c37274))
* **memory:** combine 3 node processes into one ([#5600](https://github.com/homarr-labs/homarr/issues/5600)) ([a1aa550](https://github.com/homarr-labs/homarr/commit/a1aa5505a365688b456090a882770e561479833e))
## [1.61.1](https://github.com/homarr-labs/homarr/compare/v1.61.0...v1.61.1) (2026-05-18)
### Bug Fixes
+1 -1
View File
@@ -7,7 +7,7 @@
"build": "pnpm with-env next build",
"clean": "git clean -xdf .next .turbo node_modules",
"dev": "pnpm with-env next dev",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"start": "pnpm with-env next start",
"typecheck": "tsc --noEmit",
@@ -18,15 +18,7 @@ export const CustomMantineProvider = ({
const manager = useColorSchemeManager();
return (
<DirectionProvider>
<MantineProvider
defaultColorScheme={defaultColorScheme}
colorSchemeManager={manager}
theme={createTheme({
primaryColor: "red",
autoContrast: true,
defaultRadius: "sm",
})}
>
<MantineProvider defaultColorScheme={defaultColorScheme} colorSchemeManager={manager} theme={theme}>
{children}
</MantineProvider>
</DirectionProvider>
@@ -185,7 +185,7 @@ export default async function AboutPage() {
<SimpleGrid cols={{ xs: 1, sm: 2, md: 3, lg: 4, xl: 5 }} spacing="md">
{Object.entries(dependencies)
.filter(([, value]) => !value.includes("workspace:"))
.toSorted(([key1], [key2]) => key1.localeCompare(key2))
.sort(([key1], [key2]) => key1.localeCompare(key2))
.map(([name, version]) => (
<UnstyledButton
key={name}
@@ -48,7 +48,7 @@ export default async function CertificatesPage({ params }: CertificatesPageProps
} as const;
}
})
.toSorted((certA, certB) => {
.sort((certA, certB) => {
if (certA.isError) return -1;
if (certB.isError) return 1;
return certA.x509.validToDate.getTime() - certB.x509.validToDate.getTime();
@@ -1,85 +0,0 @@
"use client";
import { Button, Group, Stack } from "@mantine/core";
import type { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { userChangeHomeBoardsSchema } from "@homarr/validation/user";
import type { Board } from "~/app/[locale]/boards/_types";
import { BoardSelect } from "~/components/board/board-select";
interface ChangeHomeBoardFormProps {
user: RouterOutputs["user"]["getById"];
boardsData: Pick<Board, "id" | "name" | "logoImageUrl">[];
}
export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormProps) => {
const t = useI18n();
const { mutate, isPending } = clientApi.user.changeHomeBoards.useMutation({
async onSettled() {
await revalidatePathActionAsync(`/manage/users/${user.id}`);
},
onSuccess(_, variables) {
form.setInitialValues({
homeBoardId: variables.homeBoardId,
mobileHomeBoardId: variables.mobileHomeBoardId,
});
showSuccessNotification({
message: t("user.action.changeHomeBoard.notification.success.message"),
});
},
onError() {
showErrorNotification({
message: t("user.action.changeHomeBoard.notification.error.message"),
});
},
});
const form = useZodForm(userChangeHomeBoardsSchema, {
initialValues: {
homeBoardId: user.homeBoardId,
mobileHomeBoardId: user.mobileHomeBoardId,
},
});
const handleSubmit = (values: FormType) => {
mutate({
userId: user.id,
...values,
});
};
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="md">
<BoardSelect
label={t("management.page.user.setting.general.item.board.type.general")}
clearable
boards={boardsData}
w="100%"
{...form.getInputProps("homeBoardId")}
/>
<BoardSelect
label={t("management.page.user.setting.general.item.board.type.mobile")}
clearable
boards={boardsData}
w="100%"
{...form.getInputProps("mobileHomeBoardId")}
/>
<Group justify="end">
<Button type="submit" loading={isPending}>
{t("common.action.save")}
</Button>
</Group>
</Stack>
</form>
);
};
type FormType = z.infer<typeof userChangeHomeBoardsSchema>;
@@ -1,84 +0,0 @@
"use client";
import { Button, Group, Select, Stack, Switch } from "@mantine/core";
import type { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { userChangeSearchPreferencesSchema } from "@homarr/validation/user";
interface ChangeSearchPreferencesFormProps {
user: RouterOutputs["user"]["getById"];
searchEnginesData: { value: string; label: string }[];
}
export const ChangeSearchPreferencesForm = ({ user, searchEnginesData }: ChangeSearchPreferencesFormProps) => {
const t = useI18n();
const { mutate, isPending } = clientApi.user.changeSearchPreferences.useMutation({
async onSettled() {
await revalidatePathActionAsync(`/manage/users/${user.id}`);
},
onSuccess(_, variables) {
form.setInitialValues({
defaultSearchEngineId: variables.defaultSearchEngineId,
openInNewTab: variables.openInNewTab,
ddgBangsEnabled: variables.ddgBangsEnabled,
});
showSuccessNotification({
message: t("user.action.changeSearchPreferences.notification.success.message"),
});
},
onError() {
showErrorNotification({
message: t("user.action.changeSearchPreferences.notification.error.message"),
});
},
});
const form = useZodForm(userChangeSearchPreferencesSchema, {
initialValues: {
defaultSearchEngineId: user.defaultSearchEngineId,
openInNewTab: user.openSearchInNewTab,
ddgBangsEnabled: user.ddgBangs,
},
});
const handleSubmit = (values: FormType) => {
mutate({
userId: user.id,
...values,
});
};
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="md">
<Select
label={t("user.field.defaultSearchEngine.label")}
w="100%"
data={searchEnginesData}
{...form.getInputProps("defaultSearchEngineId")}
/>
<Switch
label={t("user.field.openSearchInNewTab.label")}
{...form.getInputProps("openInNewTab", { type: "checkbox" })}
/>
<Switch
label={t("user.field.ddgBangs.label")}
{...form.getInputProps("ddgBangsEnabled", { type: "checkbox" })}
/>
<Group justify="end">
<Button type="submit" loading={isPending}>
{t("common.action.save")}
</Button>
</Group>
</Stack>
</form>
);
};
type FormType = z.infer<typeof userChangeSearchPreferencesSchema>;
@@ -1,83 +0,0 @@
"use client";
import { Button, Group, Radio, Stack } from "@mantine/core";
import type { DayOfWeek } from "@mantine/dates";
import dayjs from "dayjs";
import localeData from "dayjs/plugin/localeData";
import type { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { userFirstDayOfWeekSchema } from "@homarr/validation/user";
dayjs.extend(localeData);
interface FirstDayOfWeekProps {
user: RouterOutputs["user"]["getById"];
}
const weekDays = dayjs.weekdays(false);
export const FirstDayOfWeek = ({ user }: FirstDayOfWeekProps) => {
const t = useI18n();
const { mutate, isPending } = clientApi.user.changeFirstDayOfWeek.useMutation({
async onSettled() {
await revalidatePathActionAsync(`/manage/users/${user.id}`);
},
onSuccess(_, variables) {
form.setInitialValues({
firstDayOfWeek: variables.firstDayOfWeek as DayOfWeek,
});
showSuccessNotification({
message: t("user.action.changeFirstDayOfWeek.notification.success.message"),
});
},
onError() {
showErrorNotification({
message: t("user.action.changeFirstDayOfWeek.notification.error.message"),
});
},
});
const form = useZodForm(userFirstDayOfWeekSchema, {
initialValues: {
firstDayOfWeek: user.firstDayOfWeek as DayOfWeek,
},
});
const handleSubmit = (values: FormType) => {
mutate({
id: user.id,
...values,
});
};
const inputProps = form.getInputProps("firstDayOfWeek");
const onChange = inputProps.onChange as (value: number) => void;
const value = (inputProps.value as number).toString();
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="md">
<Radio.Group {...inputProps} value={value} onChange={(nextValue: string) => onChange(parseInt(nextValue, 10))}>
<Group mt="xs">
<Radio value="1" label={weekDays[1]} />
<Radio value="6" label={weekDays[6]} />
<Radio value="0" label={weekDays[0]} />
</Group>
</Radio.Group>
<Group justify="end">
<Button type="submit" loading={isPending}>
{t("common.action.save")}
</Button>
</Group>
</Stack>
</form>
);
};
type FormType = z.infer<typeof userFirstDayOfWeekSchema>;
@@ -0,0 +1,332 @@
"use client";
import { useEffect, useMemo, useRef } from "react";
import {
Button,
Card,
Divider,
Group,
Radio,
Select,
SimpleGrid,
Stack,
Switch,
Text,
TextInput,
Title,
} from "@mantine/core";
import type { DayOfWeek } from "@mantine/dates";
import dayjs from "dayjs";
import localeData from "dayjs/plugin/localeData";
import { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { env } from "@homarr/common/env";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import {
userChangeHomeBoardsSchema,
userChangeSearchPreferencesSchema,
userEditProfileSchema,
userFirstDayOfWeekSchema,
userPingIconsEnabledSchema,
} from "@homarr/validation/user";
import type { Board } from "~/app/[locale]/boards/_types";
import { BoardSelect } from "~/components/board/board-select";
import { CurrentLanguageCombobox } from "~/components/language/current-language-combobox";
dayjs.extend(localeData);
const userGeneralSettingsSchema = z.object({
name: userEditProfileSchema.shape.name,
email: userEditProfileSchema.shape.email,
homeBoardId: userChangeHomeBoardsSchema.shape.homeBoardId,
mobileHomeBoardId: userChangeHomeBoardsSchema.shape.mobileHomeBoardId,
defaultSearchEngineId: userChangeSearchPreferencesSchema.shape.defaultSearchEngineId,
openInNewTab: userChangeSearchPreferencesSchema.shape.openInNewTab,
ddgBangsEnabled: userChangeSearchPreferencesSchema.shape.ddgBangsEnabled,
firstDayOfWeek: userFirstDayOfWeekSchema.shape.firstDayOfWeek,
pingIconsEnabled: userPingIconsEnabledSchema.shape.pingIconsEnabled,
});
type FormValues = z.infer<typeof userGeneralSettingsSchema>;
const FIRST_DAY_OPTIONS: { value: DayOfWeek; labelKey: number }[] = [
{ value: 1, labelKey: 1 },
{ value: 6, labelKey: 6 },
{ value: 0, labelKey: 0 },
];
interface UserGeneralSettingsFormProps {
user: RouterOutputs["user"]["getById"];
boardsData: Pick<Board, "id" | "name" | "logoImageUrl">[];
searchEnginesData: { value: string; label: string }[];
showLanguageSelector?: boolean;
}
const buildInitialValues = (user: RouterOutputs["user"]["getById"]): FormValues => ({
name: user.name ?? "",
email: user.email ?? "",
homeBoardId: user.homeBoardId,
mobileHomeBoardId: user.mobileHomeBoardId,
defaultSearchEngineId: user.defaultSearchEngineId,
openInNewTab: user.openSearchInNewTab,
ddgBangsEnabled: user.ddgBangs,
firstDayOfWeek: user.firstDayOfWeek as DayOfWeek,
pingIconsEnabled: user.pingIconsEnabled,
});
export const UserGeneralSettingsForm = ({
user,
boardsData,
searchEnginesData,
showLanguageSelector = false,
}: UserGeneralSettingsFormProps) => {
const t = useI18n();
const tGeneral = useScopedI18n("management.page.user.setting.general");
const isCredentialsUser = user.provider === "credentials";
const editProfileMutation = clientApi.user.editProfile.useMutation();
const changeHomeBoardsMutation = clientApi.user.changeHomeBoards.useMutation();
const changeSearchPreferencesMutation = clientApi.user.changeSearchPreferences.useMutation();
const changeFirstDayOfWeekMutation = clientApi.user.changeFirstDayOfWeek.useMutation();
const changePingIconsEnabledMutation = clientApi.user.changePingIconsEnabled.useMutation();
const initialValues = buildInitialValues(user);
const initialValuesRef = useRef(initialValues);
const form = useZodForm(userGeneralSettingsSchema, {
initialValues,
});
const isDirtyRef = useRef(false);
isDirtyRef.current = form.isDirty();
const weekDays = useMemo(() => dayjs.weekdays(false), []);
const mutations = [
editProfileMutation,
changeHomeBoardsMutation,
changeSearchPreferencesMutation,
changeFirstDayOfWeekMutation,
changePingIconsEnabledMutation,
];
const isPending = mutations.some((m) => m.isPending);
useEffect(() => {
const handler = (e: BeforeUnloadEvent) => {
if (env.NODE_ENV === "development") return;
if (isDirtyRef.current) e.preventDefault();
};
window.addEventListener("beforeunload", handler);
return () => window.removeEventListener("beforeunload", handler);
}, []);
const handleSubmitAsync = async (values: FormValues) => {
const parsed = userGeneralSettingsSchema.safeParse(values);
if (!parsed.success) return;
const defaults = initialValuesRef.current;
const changed = <K extends keyof FormValues>(...fields: K[]) =>
fields.some((f) => (f === "email" ? (values[f] || null) !== (defaults[f] || null) : values[f] !== defaults[f]));
const saveActions: { when: boolean; action: () => Promise<unknown> }[] = [
{
when: isCredentialsUser && changed("name", "email"),
action: () =>
editProfileMutation.mutateAsync({ id: user.id, name: parsed.data.name, email: parsed.data.email ?? "" }),
},
{
when: changed("homeBoardId", "mobileHomeBoardId"),
action: () =>
changeHomeBoardsMutation.mutateAsync({
userId: user.id,
homeBoardId: parsed.data.homeBoardId,
mobileHomeBoardId: parsed.data.mobileHomeBoardId,
}),
},
{
when: changed("defaultSearchEngineId", "openInNewTab", "ddgBangsEnabled"),
action: () =>
changeSearchPreferencesMutation.mutateAsync({
userId: user.id,
defaultSearchEngineId: parsed.data.defaultSearchEngineId,
openInNewTab: parsed.data.openInNewTab,
ddgBangsEnabled: parsed.data.ddgBangsEnabled,
}),
},
{
when: changed("firstDayOfWeek"),
action: () =>
changeFirstDayOfWeekMutation.mutateAsync({ id: user.id, firstDayOfWeek: parsed.data.firstDayOfWeek }),
},
{
when: changed("pingIconsEnabled"),
action: () =>
changePingIconsEnabledMutation.mutateAsync({ id: user.id, pingIconsEnabled: parsed.data.pingIconsEnabled }),
},
];
const promises = saveActions.filter((s) => s.when).map((s) => s.action());
if (promises.length === 0) return;
try {
await Promise.all(promises);
const newValues = { ...parsed.data, email: parsed.data.email ?? "" };
initialValuesRef.current = newValues;
form.setInitialValues(newValues);
form.resetDirty();
await revalidatePathActionAsync(`/manage/users/${user.id}`);
showSuccessNotification({
title: t("common.notification.update.success"),
message: t("common.notification.update.success"),
});
} catch {
showErrorNotification({
title: t("common.notification.update.error"),
message: t("common.notification.update.error"),
});
}
};
const handleDiscard = () => {
form.setValues(initialValuesRef.current);
form.resetDirty();
};
const firstDayOfWeekInputProps = form.getInputProps("firstDayOfWeek");
const firstDayOfWeekOnChange = firstDayOfWeekInputProps.onChange as (value: number) => void;
const firstDayOfWeekValue = (firstDayOfWeekInputProps.value as number).toString();
return (
<form onSubmit={form.onSubmit(handleSubmitAsync)}>
<Stack gap="lg">
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="lg" verticalSpacing="lg">
<Card withBorder bg="transparent">
<Stack gap="md">
<Stack gap={2}>
<Title order={3}>{t("user.name")}</Title>
{!isCredentialsUser && (
<Text c="dimmed" size="sm">
{t("management.page.user.fieldsDisabledExternalProvider")}
</Text>
)}
</Stack>
<Divider />
<TextInput
disabled={!isCredentialsUser}
label={t("user.field.username.label")}
withAsterisk
{...form.getInputProps("name")}
/>
<TextInput
disabled={!isCredentialsUser}
label={t("user.field.email.label")}
{...form.getInputProps("email")}
/>
</Stack>
</Card>
<Card withBorder bg="transparent">
<Stack gap="md">
<Stack gap={2}>
<Title order={3}>{tGeneral("item.board.title")}</Title>
</Stack>
<Divider />
<BoardSelect
label={tGeneral("item.board.type.general")}
clearable
boards={boardsData}
w="100%"
withinPortal
{...form.getInputProps("homeBoardId")}
/>
<BoardSelect
label={tGeneral("item.board.type.mobile")}
clearable
boards={boardsData}
w="100%"
withinPortal
{...form.getInputProps("mobileHomeBoardId")}
/>
</Stack>
</Card>
<Card withBorder bg="transparent">
<Stack gap="md">
<Stack gap={2}>
<Title order={3}>{tGeneral("item.search")}</Title>
</Stack>
<Divider />
<Select
label={t("user.field.defaultSearchEngine.label")}
w="100%"
data={searchEnginesData}
comboboxProps={{ withinPortal: true }}
{...form.getInputProps("defaultSearchEngineId")}
/>
<Switch
label={t("user.field.openSearchInNewTab.label")}
{...form.getInputProps("openInNewTab", { type: "checkbox" })}
/>
<Switch
label={t("user.field.ddgBangs.label")}
{...form.getInputProps("ddgBangsEnabled", { type: "checkbox" })}
/>
</Stack>
</Card>
<Card withBorder bg="transparent">
<Stack gap="md">
<Stack gap={2}>
<Title order={3}>{tGeneral("item.language")}</Title>
</Stack>
<Divider />
{showLanguageSelector && <CurrentLanguageCombobox withinPortal />}
<Title order={4}>{tGeneral("item.firstDayOfWeek")}</Title>
<Radio.Group
{...firstDayOfWeekInputProps}
value={firstDayOfWeekValue}
onChange={(nextValue: string) => firstDayOfWeekOnChange(parseInt(nextValue, 10))}
>
<Group mt="xs" wrap="wrap">
{FIRST_DAY_OPTIONS.map(({ value: dayValue, labelKey }) => (
<Radio key={dayValue} value={dayValue.toString()} label={weekDays[labelKey]} />
))}
</Group>
</Radio.Group>
<Divider my="xs" />
<Title order={4}>{tGeneral("item.accessibility")}</Title>
<Switch
label={t("user.field.pingIconsEnabled.label")}
{...form.getInputProps("pingIconsEnabled", { type: "checkbox" })}
/>
</Stack>
</Card>
</SimpleGrid>
<div style={{ position: "sticky", bottom: 20 }}>
{form.isDirty() && (
<Card withBorder>
<Group justify="space-between" wrap="wrap">
<Text fw={500}>{t("common.unsavedChanges")}</Text>
<Group>
<Button disabled={isPending} variant="default" onClick={handleDiscard}>
{t("common.action.discard")}
</Button>
<Button loading={isPending} type="submit" disabled={!form.isValid()}>
{t("common.action.saveChanges")}
</Button>
</Group>
</Group>
</Card>
)}
</div>
</Stack>
</form>
);
};
@@ -1,69 +0,0 @@
"use client";
import { Button, Group, Stack, Switch } from "@mantine/core";
import type { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { userPingIconsEnabledSchema } from "@homarr/validation/user";
interface PingIconsEnabledProps {
user: RouterOutputs["user"]["getById"];
}
export const PingIconsEnabled = ({ user }: PingIconsEnabledProps) => {
const t = useI18n();
const { mutate, isPending } = clientApi.user.changePingIconsEnabled.useMutation({
async onSettled() {
await revalidatePathActionAsync(`/manage/users/${user.id}`);
},
onSuccess(_, variables) {
form.setInitialValues({
pingIconsEnabled: variables.pingIconsEnabled,
});
showSuccessNotification({
message: t("user.action.changePingIconsEnabled.notification.success.message"),
});
},
onError() {
showErrorNotification({
message: t("user.action.changePingIconsEnabled.notification.error.message"),
});
},
});
const form = useZodForm(userPingIconsEnabledSchema, {
initialValues: {
pingIconsEnabled: user.pingIconsEnabled,
},
});
const handleSubmit = (values: FormType) => {
mutate({
id: user.id,
...values,
});
};
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="md">
<Switch
{...form.getInputProps("pingIconsEnabled", { type: "checkbox" })}
label={t("user.field.pingIconsEnabled.label")}
/>
<Group justify="end">
<Button type="submit" loading={isPending}>
{t("common.action.save")}
</Button>
</Group>
</Stack>
</form>
);
};
type FormType = z.infer<typeof userPingIconsEnabledSchema>;
@@ -2,7 +2,7 @@
import { useCallback } from "react";
import { Box, Button, FileButton, Menu, UnstyledButton } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { useDisclosure, useMediaQuery } from "@mantine/hooks";
import { IconPencil, IconPhotoEdit, IconPhotoX } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
@@ -25,6 +25,7 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
},
});
const [opened, { toggle }] = useDisclosure(false);
const isMobile = useMediaQuery("(max-width: 48em)");
const { openConfirmModal } = useConfirmModal();
const t = useI18n();
const tManageAvatar = useScopedI18n("user.action.manageAvatar");
@@ -94,11 +95,11 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
}, [mutate, user.id, openConfirmModal, tManageAvatar]);
return (
<Box pos="relative">
<Menu opened={opened} keepMounted onChange={toggle} position="bottom-start">
<Box pos="relative" display="flex" style={{ justifyContent: isMobile ? "center" : undefined }}>
<Menu opened={opened} keepMounted onChange={toggle} position="bottom-start" withArrow withinPortal>
<Menu.Target>
<UnstyledButton onClick={toggle}>
<UserAvatar user={user} size={200} />
<UserAvatar user={user} size={isMobile ? 120 : 200} />
<Button
component="div"
pos="absolute"
@@ -1,94 +0,0 @@
"use client";
import { useCallback } from "react";
import { Button, Group, Stack, TextInput } from "@mantine/core";
import type { RouterInputs, RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { userEditProfileSchema } from "@homarr/validation/user";
interface UserProfileFormProps {
user: RouterOutputs["user"]["getById"];
}
export const UserProfileForm = ({ user }: UserProfileFormProps) => {
const t = useI18n();
const { mutate, isPending } = clientApi.user.editProfile.useMutation({
async onSettled() {
await revalidatePathActionAsync("/manage/users");
},
onSuccess(_, variables) {
// Reset form initial values to reset dirty state
form.setInitialValues({
name: variables.name,
email: variables.email ?? "",
});
showSuccessNotification({
title: t("common.notification.update.success"),
message: t("user.action.editProfile.notification.success.message"),
});
},
onError(error) {
const message =
error.data?.code === "CONFLICT"
? t("user.error.usernameTaken")
: t("user.action.editProfile.notification.error.message");
showErrorNotification({
title: t("common.notification.update.error"),
message,
});
},
});
const form = useZodForm(userEditProfileSchema.omit({ id: true }), {
initialValues: {
name: user.name ?? "",
email: user.email ?? "",
},
});
// Only credentials users can edit their profile
const isProviderCredentials = user.provider === "credentials";
const handleSubmit = useCallback(
(values: FormType) => {
if (!isProviderCredentials) return;
mutate({
...values,
id: user.id,
});
},
[isProviderCredentials, mutate, user.id],
);
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack>
<TextInput
disabled={!isProviderCredentials}
label={t("user.field.username.label")}
withAsterisk
{...form.getInputProps("name")}
/>
<TextInput
disabled={!isProviderCredentials}
label={t("user.field.email.label")}
{...form.getInputProps("email")}
/>
{isProviderCredentials && (
<Group justify="end">
<Button type="submit" disabled={!form.isDirty()} loading={isPending}>
{t("common.action.saveChanges")}
</Button>
</Group>
)}
</Stack>
</form>
);
};
type FormType = Omit<RouterInputs["user"]["editProfile"], "id">;
@@ -6,18 +6,13 @@ import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { getI18n, getScopedI18n } from "@homarr/translation/server";
import { CurrentLanguageCombobox } from "~/components/language/current-language-combobox";
import { DangerZoneItem, DangerZoneRoot } from "~/components/manage/danger-zone";
import { catchTrpcNotFound } from "~/errors/trpc-catch-error";
import { createMetaTitle } from "~/metadata";
import { canAccessUserEditPage } from "../access";
import { ChangeHomeBoardForm } from "./_components/_change-home-board";
import { ChangeSearchPreferencesForm } from "./_components/_change-search-preferences";
import { DeleteUserButton } from "./_components/_delete-user-button";
import { FirstDayOfWeek } from "./_components/_first-day-of-week";
import { PingIconsEnabled } from "./_components/_ping-icons-enabled";
import { UserGeneralSettingsForm } from "./_components/_general-settings-form";
import { UserProfileAvatarForm } from "./_components/_profile-avatar-form";
import { UserProfileForm } from "./_components/_profile-form";
interface Props {
params: Promise<{
@@ -63,7 +58,7 @@ export default async function EditUserPage(props: Props) {
const boards = await api.board.getAllBoards();
const searchEngines = await api.searchEngine.getSelectable();
const isSelf = session?.user.id === user.id;
const isCredentialsUser = user.provider === "credentials";
return (
@@ -73,51 +68,25 @@ export default async function EditUserPage(props: Props) {
{t("management.page.user.fieldsDisabledExternalProvider")}
</Alert>
)}
<Title>{tGeneral("title")}</Title>
<Group gap="xl">
<Box flex={1}>
<UserProfileForm user={user} />
<Group gap="xl" align="flex-start" wrap="wrap">
<Box flex={1} miw={{ base: "100%", md: 540 }}>
<UserGeneralSettingsForm
user={user}
boardsData={boards.map((board) => ({
id: board.id,
name: board.name,
logoImageUrl: board.logoImageUrl,
}))}
searchEnginesData={searchEngines}
showLanguageSelector={isSelf}
/>
</Box>
<Box w={{ base: "100%", lg: 200 }}>
<Box w={{ base: "100%", lg: 260 }}>
<UserProfileAvatarForm user={user} />
</Box>
</Group>
{session?.user.id === user.id && (
<Stack mb="lg">
<Title order={2}>{tGeneral("item.language")}</Title>
<CurrentLanguageCombobox />
</Stack>
)}
<Stack mb="lg">
<Title order={2}>{tGeneral("item.board.title")}</Title>
<ChangeHomeBoardForm
user={user}
boardsData={boards.map((board) => ({
id: board.id,
name: board.name,
logoImageUrl: board.logoImageUrl,
}))}
/>
</Stack>
<Stack mb="lg">
<Title order={2}>{tGeneral("item.search")}</Title>
<ChangeSearchPreferencesForm user={user} searchEnginesData={searchEngines} />
</Stack>
<Stack mb="lg">
<Title order={2}>{tGeneral("item.firstDayOfWeek")}</Title>
<FirstDayOfWeek user={user} />
</Stack>
<Stack mb="lg">
<Title order={2}>{tGeneral("item.accessibility")}</Title>
<PingIconsEnabled user={user} />
</Stack>
<DangerZoneRoot>
<DangerZoneItem
label={t("user.action.delete.label")}
@@ -6,11 +6,20 @@ import { LanguageCombobox } from "./language-combobox";
interface CurrentLanguageComboboxProps {
width?: string;
withinPortal?: boolean;
}
export const CurrentLanguageCombobox = ({ width }: CurrentLanguageComboboxProps) => {
export const CurrentLanguageCombobox = ({ width, withinPortal }: CurrentLanguageComboboxProps) => {
const currentLocale = useCurrentLocale();
const { changeLocale, isPending } = useChangeLocale();
return <LanguageCombobox value={currentLocale} onChange={changeLocale} isPending={isPending} width={width} />;
return (
<LanguageCombobox
value={currentLocale}
onChange={changeLocale}
isPending={isPending}
width={width}
withinPortal={withinPortal}
/>
);
};
@@ -16,9 +16,17 @@ interface LanguageComboboxProps {
onChange: (value: SupportedLanguage) => void;
isPending?: boolean;
width?: string;
withinPortal?: boolean;
}
export const LanguageCombobox = ({ label, value, onChange, isPending, width }: LanguageComboboxProps) => {
export const LanguageCombobox = ({
label,
value,
onChange,
isPending,
width,
withinPortal = false,
}: LanguageComboboxProps) => {
const combobox = useCombobox({
onDropdownClose: () => combobox.resetSelectedOption(),
});
@@ -39,7 +47,7 @@ export const LanguageCombobox = ({ label, value, onChange, isPending, width }: L
}, [combobox]);
return (
<Combobox store={combobox} onOptionSubmit={handleOnOptionSubmit}>
<Combobox store={combobox} withinPortal={withinPortal} onOptionSubmit={handleOnOptionSubmit}>
<Combobox.Target>
<InputBase
component="button"
+1 -1
View File
@@ -12,7 +12,7 @@
"scripts": {
"clean": "rm -rf .turbo node_modules",
"dev": "pnpm with-env tsx ./src/main.ts",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit",
"with-env": "dotenv -e ../../.env --"
+1 -1
View File
@@ -12,7 +12,7 @@
"scripts": {
"clean": "rm -rf .turbo node_modules",
"dev": "pnpm with-env tsx ./src/main.ts",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit",
"with-env": "dotenv -e ../../.env --"
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "homarr",
"version": "1.61.1",
"version": "1.63.0",
"private": true,
"scripts": {
"build": "cross-env CI=true turbo build",
@@ -17,8 +17,8 @@
"db:studio": "pnpm -F db studio",
"dev": "pnpm -F nextjs dev",
"docker:dev": "docker compose -f ./development/development.docker-compose.yml up",
"format": "turbo format --continue",
"format:fix": "turbo format --continue -- --write",
"format": "turbo format --continue -- --check",
"format:fix": "turbo format --continue",
"lint": "turbo lint --continue",
"lint:fix": "turbo lint --continue -- --fix",
"lint:ws": "pnpm dlx sherif@latest",
+1 -1
View File
@@ -16,7 +16,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -16,7 +16,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+3
View File
@@ -40,6 +40,9 @@ export const env = createEnv({
AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE: z.string().optional(),
AUTH_OIDC_FORCE_USERINFO: createBooleanSchema(false),
AUTH_OIDC_ENABLE_DANGEROUS_ACCOUNT_LINKING: createBooleanSchema(false),
AUTH_OIDC_TOKEN_ENDPOINT_AUTH_METHOD: z
.enum(["client_secret_basic", "client_secret_post", "client_secret_jwt", "none"])
.default("client_secret_basic"),
}
: {}),
...(authProviders.includes("ldap")
+1 -1
View File
@@ -18,7 +18,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
@@ -14,6 +14,9 @@ export const OidcProvider = (headers: ReadonlyHeaders | null): OIDCConfig<Profil
type: "oidc",
clientId: env.AUTH_OIDC_CLIENT_ID,
clientSecret: env.AUTH_OIDC_CLIENT_SECRET,
client: {
token_endpoint_auth_method: env.AUTH_OIDC_TOKEN_ENDPOINT_AUTH_METHOD,
},
issuer: env.AUTH_OIDC_ISSUER,
allowDangerousEmailAccountLinking: env.AUTH_OIDC_ENABLE_DANGEROUS_ACCOUNT_LINKING,
authorization: {
+1 -1
View File
@@ -18,7 +18,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -17,7 +17,7 @@
"scripts": {
"build": "esbuild src/index.ts --bundle --platform=node --outfile=cli.cjs --external:bcrypt --external:cpu-features --loader:.html=text --loader:.node=text",
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"run": "pnpm with-env node cli.cjs",
"typecheck": "tsc --noEmit",
+1 -1
View File
@@ -20,7 +20,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -31,7 +31,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -17,7 +17,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -18,7 +18,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -17,7 +17,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
@@ -1,13 +1,11 @@
import { EVERY_HOUR } from "@homarr/cron-jobs-core/expressions";
import { EVERY_DAY } from "@homarr/cron-jobs-core/expressions";
import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker";
import { createCronJob } from "../lib";
export const updateCheckerJob = createCronJob("updateChecker", EVERY_HOUR, {
export const updateCheckerJob = createCronJob("updateChecker", EVERY_DAY, {
runOnStart: true,
}).withCallback(async () => {
const handler = updateCheckerRequestHandler.handler({});
await handler.getCachedOrUpdatedDataAsync({
forceUpdate: true,
});
await handler.getCachedOrUpdatedDataAsync({});
});
+1 -1
View File
@@ -20,7 +20,7 @@
"build:postgresql": "esbuild migrations/postgresql/migrate.ts --bundle --platform=node --outfile=migrations/postgresql/migrate.cjs",
"build:sqlite": "esbuild migrations/sqlite/migrate.ts --bundle --platform=node --outfile=migrations/sqlite/migrate.cjs",
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"migration:custom": "pnpm with-env tsx ./migrations/custom/run-custom.ts",
"migration:mysql:drop": "pnpm with-env drizzle-kit drop --config ./configs/mysql.config.ts",
+1 -1
View File
@@ -16,7 +16,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"postinstall": "tsx ./src/docs/codegen.ts",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
+1 -1
View File
@@ -18,7 +18,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -17,7 +17,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -16,7 +16,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -17,7 +17,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -16,7 +16,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -19,7 +19,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -16,7 +16,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -16,7 +16,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -17,7 +17,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -20,7 +20,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -16,7 +16,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -16,7 +16,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -16,7 +16,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -16,7 +16,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
@@ -1,6 +1,7 @@
import dayjs from "dayjs";
import type { Duration } from "dayjs/plugin/duration";
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
import { createLogger } from "@homarr/core/infrastructure/logs";
import type { createChannelWithLatestAndEvents } from "@homarr/redis";
@@ -15,6 +16,7 @@ interface Options<TData, TInput extends Record<string, unknown>> {
options: Options<TData, TInput>,
) => ReturnType<typeof createChannelWithLatestAndEvents<TData>>;
cacheDuration: Duration;
fallbackToStaleOnError?: boolean;
}
export const createCachedRequestHandler = <TData, TInput extends Record<string, unknown>>(
@@ -26,13 +28,33 @@ export const createCachedRequestHandler = <TData, TInput extends Record<string,
return {
async getCachedOrUpdatedDataAsync({ forceUpdate = false }) {
const channelData = await channel.getAsync();
const requestNewDataAsync = async () => {
const data = await options.requestAsync(input);
await channel.publishAndUpdateLastStateAsync(data);
return {
data,
timestamp: new Date(),
};
try {
const data = await options.requestAsync(input);
await channel.publishAndUpdateLastStateAsync(data);
return {
data,
timestamp: new Date(),
};
} catch (error) {
if (options.fallbackToStaleOnError && channelData) {
logger.warn(
new ErrorWithMetadata(
"Cached request handler using stale cache after fetch failure",
{
channel: channel.name,
queryKey: options.queryKey,
},
{ cause: error },
),
);
return channelData;
}
throw error;
}
};
if (forceUpdate) {
@@ -43,8 +65,6 @@ export const createCachedRequestHandler = <TData, TInput extends Record<string,
return await requestNewDataAsync();
}
const channelData = await channel.getAsync();
const shouldRequestNewData =
!channelData ||
dayjs().diff(channelData.timestamp, "milliseconds") > options.cacheDuration.asMilliseconds();
@@ -111,6 +111,24 @@ describe("getAvailableUpdatesAsync", () => {
// Assert
expect(result).toEqual([]);
});
test("should propagate GitHub API auth errors without blocking callers", async () => {
const currentVersion = "v1.2.3";
const listReleasesSpy = vi.spyOn(new Octokit().rest.repos, "listReleases");
listReleasesSpy.mockRejectedValue(new Error("Bad credentials"));
await expect(getAvailableUpdatesAsync(currentVersion)).rejects.toThrow("Bad credentials");
});
test("should propagate GitHub API quota exhausted errors without blocking callers", async () => {
const currentVersion = "v1.2.3";
const listReleasesSpy = vi.spyOn(new Octokit().rest.repos, "listReleases");
listReleasesSpy.mockRejectedValue(
new Error("Request quota exhausted for request GET /repos/{owner}/{repo}/releases"),
);
await expect(getAvailableUpdatesAsync(currentVersion)).rejects.toThrow("quota exhausted");
});
});
const fakeReleases = (...inputs: [string, boolean][]) => inputs.map(fakeRelease);
@@ -15,7 +15,8 @@ const logger = createLogger({ module: "updateCheckerRequestHandler" });
export const updateCheckerRequestHandler = createCachedRequestHandler({
queryKey: "homarr-update-checker",
cacheDuration: dayjs.duration(1, "hour"),
cacheDuration: dayjs.duration(1, "day"),
fallbackToStaleOnError: true,
async requestAsync(_) {
return {
availableUpdates: await getAvailableUpdatesAsync(packageJson.version),
@@ -58,6 +59,7 @@ export const getAvailableUpdatesAsync = async (currentVersion: string) => {
request: {
fetch: fetchWithTrustedCertificatesAsync,
},
throttle: { enabled: false },
});
const isCurrentPrerelease = isPrereleaseTag(currentVersion);
@@ -100,7 +102,7 @@ export const getAvailableUpdatesAsync = async (currentVersion: string) => {
const availableUpdates = semanticReleases
.filter((release) => isCurrentPrerelease || !release.isPrerelease)
.filter((release) => compareSemVer(release.tagName, currentVersion) > 0)
.sort((releaseA, releaseB) => compareSemVer(releaseB.tagName, releaseA.tagName));
.toSorted((releaseA, releaseB) => compareSemVer(releaseB.tagName, releaseA.tagName));
if (availableUpdates.length === 0) {
logger.debug("No available updates found", { currentVersion });
+1 -1
View File
@@ -16,7 +16,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -17,7 +17,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -17,7 +17,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -21,7 +21,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": ""
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "",
"checkLogs": "",
"tryAgain": "",
"loading": ""
"loading": "",
"show": "",
"hide": ""
},
"here": "",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "",
"changePosition": ""
"changePosition": "",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "打开媒体"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "查阅文档",
"checkLogs": "检查日志获取更多详情",
"tryAgain": "重试",
"loading": "正在加载"
"loading": "正在加载",
"show": "",
"hide": ""
},
"here": "这里",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "新建分类",
"changePosition": "换位"
"changePosition": "换位",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "此面板是公开的",
"private": "此面板是私有的"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "crwdns2680:0crwdne2680:0"
}
},
"noResults": {
"title": "crwdns4251:0crwdne4251:0"
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "crwdns546:0crwdne546:0",
"checkLogs": "crwdns2260:0crwdne2260:0",
"tryAgain": "crwdns548:0crwdne548:0",
"loading": "crwdns550:0crwdne550:0"
"loading": "crwdns550:0crwdne550:0",
"show": "crwdns4255:0crwdne4255:0",
"hide": "crwdns4257:0crwdne4257:0"
},
"here": "crwdns552:0crwdne552:0",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "crwdns686:0crwdne686:0",
"changePosition": "crwdns688:0crwdne688:0"
"changePosition": "crwdns688:0crwdne688:0",
"settings": "crwdns4259:0crwdne4259:0"
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "crwdns1656:0crwdne1656:0",
"private": "crwdns1658:0crwdne1658:0"
},
"noResults": {
"title": "crwdns4253:0crwdne4253:0"
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": ""
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Přečtěte si dokumentaci",
"checkLogs": "Pro více podrobností zkontrolujte logy",
"tryAgain": "Zkusit znovu",
"loading": "Načítání"
"loading": "Načítání",
"show": "",
"hide": ""
},
"here": "zde",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Nová kategorie",
"changePosition": "Změnit pozici"
"changePosition": "Změnit pozici",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Åben medie"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Tjek dokumentationen",
"checkLogs": "Tjek logs for flere detaljer",
"tryAgain": "Prøv igen",
"loading": "Indlæser"
"loading": "Indlæser",
"show": "",
"hide": ""
},
"here": "her",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Ny kategori",
"changePosition": "Ændre placering"
"changePosition": "Ændre placering",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Denne tavle er offentlig",
"private": "Denne tavle er privat"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Medium öffnen"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Die Dokumentation ansehen",
"checkLogs": "Logs für weitere Details prüfen",
"tryAgain": "Erneut versuchen",
"loading": "Wird geladen"
"loading": "Wird geladen",
"show": "",
"hide": ""
},
"here": "Hier",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Neue Kategorie",
"changePosition": "Position wechseln"
"changePosition": "Position wechseln",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Dieses Board ist öffentlich",
"private": "Dieses Board ist privat"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Medium öffnen"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Die Dokumentation ansehen",
"checkLogs": "Logs für weitere Details prüfen",
"tryAgain": "Erneut versuchen",
"loading": "Wird geladen"
"loading": "Wird geladen",
"show": "",
"hide": ""
},
"here": "Hier",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Neue Kategorie",
"changePosition": "Position wechseln"
"changePosition": "Position wechseln",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Dieses Board ist öffentlich",
"private": "Dieses Board ist privat"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": ""
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "",
"checkLogs": "",
"tryAgain": "Προσπαθήστε ξανά",
"loading": ""
"loading": "",
"show": "",
"hide": ""
},
"here": "",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "",
"changePosition": "Αλλαγή θέσης"
"changePosition": "Αλλαγή θέσης",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Open media"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Check the documentation",
"checkLogs": "Check logs for more information",
"tryAgain": "Try again",
"loading": "Loading"
"loading": "Loading",
"show": "",
"hide": ""
},
"here": "here",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "New category",
"changePosition": "Change position"
"changePosition": "Change position",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Abrir imagen"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Consultar la documentación",
"checkLogs": "Comprueba los registros para más detalles",
"tryAgain": "Intentar de nuevo",
"loading": "Cargando"
"loading": "Cargando",
"show": "",
"hide": ""
},
"here": "aquí",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Nueva Categoría",
"changePosition": "Cambiar posición"
"changePosition": "Cambiar posición",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Este tablero es público",
"private": "Este tablero es privado"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": ""
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "",
"checkLogs": "",
"tryAgain": "",
"loading": ""
"loading": "",
"show": "",
"hide": ""
},
"here": "",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "",
"changePosition": ""
"changePosition": "",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Ouvrir le média"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Voir la documentation",
"checkLogs": "Vérifiez les logs pour plus de détails",
"tryAgain": "Réessayer",
"loading": "Chargement"
"loading": "Chargement",
"show": "",
"hide": ""
},
"here": "ici",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Nouvelle catégorie",
"changePosition": "Modifier la position"
"changePosition": "Modifier la position",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Ce tableau est public",
"private": "Ce tableau est privé"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "פתיחת מדיה"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "בדוק את התיעוד",
"checkLogs": "בדוק לוגים לפרטים נוספים",
"tryAgain": "נא לנסות שוב",
"loading": "טוען"
"loading": "טוען",
"show": "",
"hide": ""
},
"here": "כאן",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "קטגוריה חדשה",
"changePosition": "שנה מיקום"
"changePosition": "שנה מיקום",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "הלוח הזה ציבורי",
"private": "הלוח הזה פרטי"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": ""
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "",
"checkLogs": "",
"tryAgain": "Pokušaj ponovno",
"loading": ""
"loading": "",
"show": "",
"hide": ""
},
"here": "",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "",
"changePosition": "Promijenjen položaj"
"changePosition": "Promijenjen položaj",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Média megnyitása"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Tekintse meg a dokumentációt",
"checkLogs": "További részletekért ellenőrizze a naplókat",
"tryAgain": "Próbálja újra",
"loading": "Töltés"
"loading": "Töltés",
"show": "",
"hide": ""
},
"here": "itt",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Új kategória",
"changePosition": "Pozíció módosítása"
"changePosition": "Pozíció módosítása",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Apri media"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Consulta la documentazione",
"checkLogs": "Controlla i log per maggiori dettagli",
"tryAgain": "Riprova",
"loading": "Caricamento"
"loading": "Caricamento",
"show": "",
"hide": ""
},
"here": "qui",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Nuova categoria",
"changePosition": "Cambia posizione"
"changePosition": "Cambia posizione",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Questa board è pubblica",
"private": "Questa board è privata"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "メディアを開く"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "ドキュメントを確認する",
"checkLogs": "詳細はログを確認してください",
"tryAgain": "リトライ",
"loading": "読み込み中"
"loading": "読み込み中",
"show": "",
"hide": ""
},
"here": "こちら",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "新しいカテゴリー",
"changePosition": "ポジションを変更する"
"changePosition": "ポジションを変更する",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "このボードは公開中です",
"private": "このボードは非公開です"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": ""
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "",
"checkLogs": "",
"tryAgain": "다시 시도",
"loading": ""
"loading": "",
"show": "",
"hide": ""
},
"here": "",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "",
"changePosition": "위치 변경"
"changePosition": "위치 변경",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": ""
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "",
"checkLogs": "",
"tryAgain": "",
"loading": ""
"loading": "",
"show": "",
"hide": ""
},
"here": "",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "",
"changePosition": ""
"changePosition": "",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": ""
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "",
"checkLogs": "",
"tryAgain": "Mēģiniet vēlreiz",
"loading": ""
"loading": "",
"show": "",
"hide": ""
},
"here": "",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "",
"changePosition": "Mainīt pozīciju"
"changePosition": "Mainīt pozīciju",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Media openen"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "De documentatie bekijken",
"checkLogs": "Controleer logs voor meer details",
"tryAgain": "Probeer opnieuw",
"loading": "Laden"
"loading": "Laden",
"show": "",
"hide": ""
},
"here": "hier",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Nieuwe categorie",
"changePosition": "Positie wijzigen"
"changePosition": "Positie wijzigen",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Dit bord is openbaar",
"private": "Dit bord is privé"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Åpne medie"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Les dokumentasjonen",
"checkLogs": "Sjekk loggene for mer informasjon",
"tryAgain": "Prøv igjen",
"loading": "Laster"
"loading": "Laster",
"show": "",
"hide": ""
},
"here": "her",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Ny kategori",
"changePosition": "Endre posisjon"
"changePosition": "Endre posisjon",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Denne tavlen er offentlig",
"private": "Denne tavlen er privat"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Otwórz media"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Sprawdź dokumentację",
"checkLogs": "Sprawdź logi, aby uzyskać więcej informacji.",
"tryAgain": "Spróbuj ponownie",
"loading": "Ładowanie"
"loading": "Ładowanie",
"show": "",
"hide": ""
},
"here": "tutaj",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Nowa kategoria",
"changePosition": "Zmiana pozycji"
"changePosition": "Zmiana pozycji",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Ta tablica jest publiczna",
"private": "Ta tablica jest prywatna"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Abrir mídia"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Confira a documentação",
"checkLogs": "Verifique os logs para mais detalhes",
"tryAgain": "Tente novamente",
"loading": "Carregando"
"loading": "Carregando",
"show": "",
"hide": ""
},
"here": "aqui",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Nova categoria",
"changePosition": "Mudar de posição"
"changePosition": "Mudar de posição",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Este quadro é publico",
"private": "Este quadro é privado"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": ""
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "",
"checkLogs": "",
"tryAgain": "",
"loading": ""
"loading": "",
"show": "",
"hide": ""
},
"here": "",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "",
"changePosition": ""
"changePosition": "",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": ""
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "",
"checkLogs": "",
"tryAgain": "Încearcă din nou",
"loading": ""
"loading": "",
"show": "",
"hide": ""
},
"here": "",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "",
"changePosition": "Schimbă locația"
"changePosition": "Schimbă locația",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Открыть медиа"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Посмотреть документацию",
"checkLogs": "Проверить логи для подробностей",
"tryAgain": "Попробовать снова",
"loading": "Загрузка"
"loading": "Загрузка",
"show": "",
"hide": ""
},
"here": "здесь",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Новая категория",
"changePosition": "Изменить позицию"
"changePosition": "Изменить позицию",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Эта панель публичная",
"private": "Эта панель приватная"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Otvoriť médiá"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Pozrite si dokumentáciu",
"checkLogs": "Ďalšie podrobnosti nájdete v protokoloch",
"tryAgain": "Skúste znova",
"loading": "Nahrávam"
"loading": "Nahrávam",
"show": "",
"hide": ""
},
"here": "tu",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Nová kategória",
"changePosition": "Zmeniť pozíciu"
"changePosition": "Zmeniť pozíciu",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Táto tabuľa je verejná",
"private": "Táto tabuľa je súkromná"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": ""
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "",
"checkLogs": "",
"tryAgain": "Poskusite znova",
"loading": ""
"loading": "",
"show": "",
"hide": ""
},
"here": "",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "",
"changePosition": "Spremeni položaj"
"changePosition": "Spremeni položaj",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": ""
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "",
"checkLogs": "",
"tryAgain": "Försök igen",
"loading": ""
"loading": "",
"show": "",
"hide": ""
},
"here": "",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "",
"changePosition": "Ändra position"
"changePosition": "Ändra position",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Den här tavlan är offentlig",
"private": "Den här tavlan är privat"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Medyayı aç"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Dökümanlara göz at",
"checkLogs": "Daha fazla ayrıntı için günlükleri kontrol edin",
"tryAgain": "Tekrar Deneyin",
"loading": "Yükleniyor"
"loading": "Yükleniyor",
"show": "",
"hide": ""
},
"here": "buraya",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Yeni Kategori",
"changePosition": "Pozisyonu değiştir"
"changePosition": "Pozisyonu değiştir",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Bu panel herkese açıktır",
"private": "Bu panel özeldir"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Відкрити медіа"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Переглянути документацію",
"checkLogs": "Щоб дізнатися більше, перевірте логи",
"tryAgain": "Повторіть спробу",
"loading": "Завантаження"
"loading": "Завантаження",
"show": "",
"hide": ""
},
"here": "тут",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Нова категорія",
"changePosition": "Змінити положення"
"changePosition": "Змінити положення",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Ця дошка є публічною",
"private": "Ця дошка є приватною"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": ""
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "",
"checkLogs": "",
"tryAgain": "Thử lại",
"loading": ""
"loading": "",
"show": "",
"hide": ""
},
"here": "",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "",
"changePosition": "Đổi vị trí"
"changePosition": "Đổi vị trí",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "開啟多媒體"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "查閱說明書",
"checkLogs": "檢查 Logs 以取得更多詳情",
"tryAgain": "重試",
"loading": "正在讀取"
"loading": "正在讀取",
"show": "",
"hide": ""
},
"here": "此處",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "創建分類",
"changePosition": "變更位置"
"changePosition": "變更位置",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "此面板是公開的",
"private": "此面板是私人的"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+1 -1
View File
@@ -19,7 +19,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
@@ -20,6 +20,7 @@ export interface SelectWithCustomItemsProps<TSelectItem extends BaseSelectItem>
onBlur?: (event: React.FocusEvent<HTMLButtonElement>) => void;
onFocus?: (event: React.FocusEvent<HTMLButtonElement>) => void;
w?: string;
withinPortal?: boolean;
}
type Props<TSelectItem extends BaseSelectItem> = SelectWithCustomItemsProps<TSelectItem> & {
@@ -35,6 +36,7 @@ export const SelectWithCustomItems = <TSelectItem extends BaseSelectItem>({
SelectOption,
w,
clearable,
withinPortal = false,
...props
}: Props<TSelectItem>) => {
const combobox = useCombobox({
@@ -71,7 +73,7 @@ export const SelectWithCustomItems = <TSelectItem extends BaseSelectItem>({
const _clearable = clearable && Boolean(_value);
return (
<Combobox store={combobox} withinPortal={false} onOptionSubmit={onOptionSubmit}>
<Combobox store={combobox} withinPortal={withinPortal} onOptionSubmit={onOptionSubmit}>
<Combobox.Target>
<InputBase
{...props}
+1 -1
View File
@@ -16,7 +16,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -19,7 +19,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
@@ -39,7 +39,7 @@ export const ResourceTable = ({ type, data, isTiny }: ResourceTableProps) => {
</TableThead>
<TableTbody>
{data
.toSorted((itemA, itemB) => {
.sort((itemA, itemB) => {
const nodeResult = itemA.node.localeCompare(itemB.node);
if (nodeResult !== 0) return nodeResult;
return itemA.name.localeCompare(itemB.name);
@@ -43,7 +43,7 @@ export default function MediaServerWidget({
const newData = filteredData.concat(
data.requests.map((request) => ({ ...request, integrationId: data.integrationId })),
);
return newData.toSorted((dataA, dataB) => {
return newData.sort((dataA, dataB) => {
if (dataA.status === dataB.status) {
return dataB.createdAt.getTime() - dataA.createdAt.getTime();
}
+1 -1
View File
@@ -26,7 +26,7 @@ const useLiveFeedEntries = (input: RouterInputs["widget"]["rssFeed"]["getFeeds"]
return oldData
?.filter((entry) => entry.feedUrl !== updatedData.url)
.concat(updatedData.entries)
.toSorted((entryA, entryB) => {
.sort((entryA, entryB) => {
return entryA.published && entryB.published
? new Date(entryB.published).getTime() - new Date(entryA.published).getTime()
: 0;
+55 -24
View File
@@ -475,8 +475,8 @@ catalogs:
specifier: ^2.2.0
version: 2.2.0
trpc-to-openapi:
specifier: ^3.2.0
version: 3.2.0
specifier: ^3.3.0
version: 3.3.0
tsdav:
specifier: ^2.2.2
version: 2.2.2
@@ -1153,7 +1153,7 @@ importers:
version: 2.2.6
trpc-to-openapi:
specifier: 'catalog:'
version: 3.2.0(patch_hash=2ca3c16af0fcca0c736697ad4fe553a14f794524fa9ce0d5c3e8ee4aea76090c)(@trpc/server@11.17.0(typescript@5.9.3))(zod-openapi@5.3.0(zod@4.4.3))(zod@4.4.3)
version: 3.3.0(patch_hash=2ca3c16af0fcca0c736697ad4fe553a14f794524fa9ce0d5c3e8ee4aea76090c)(@trpc/server@11.17.0(typescript@5.9.3))(zod-openapi@5.3.0(zod@4.4.3))(zod@4.4.3)
zod:
specifier: 'catalog:'
version: 4.4.3
@@ -4871,6 +4871,13 @@ packages:
'@tabler/icons@3.44.0':
resolution: {integrity: sha512-Wn0AOZG9sg0L+bjfMqq4eNhC6pQjIrk94LvvWYNYkY8KH8wC3YILRzQlrnVJc4FUeMxH/AK97QsYCX35H3LndA==}
'@tanstack/devtools-event-client@0.4.0':
resolution: {integrity: sha512-RPfGuk2bDZgcu9bAJodvO2lnZeHuz4/71HjZ0bGb/SPg8+lyTA+RLSKQvo7fSmPSi8/vcH3aKQ8EM9ywf1olaw==}
engines: {node: '>=18'}
'@tanstack/form-core@1.27.7':
resolution: {integrity: sha512-nvogpyE98fhb0NDw1Bf2YaCH+L7ZIUgEpqO9TkHucDn6zg3ni521boUpv0i8HKIrmmFwDYjWZoCnrgY4HYWTkw==}
'@tanstack/match-sorter-utils@8.19.4':
resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==}
engines: {node: '>=12'}
@@ -4899,6 +4906,12 @@ packages:
peerDependencies:
react: ^18 || ^19
'@tanstack/react-store@0.8.0':
resolution: {integrity: sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@tanstack/react-table@8.20.5':
resolution: {integrity: sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA==}
engines: {node: '>=12'}
@@ -4912,6 +4925,12 @@ packages:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@tanstack/store@0.7.7':
resolution: {integrity: sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==}
'@tanstack/store@0.8.0':
resolution: {integrity: sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ==}
'@tanstack/table-core@8.20.5':
resolution: {integrity: sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==}
engines: {node: '>=12'}
@@ -6082,8 +6101,8 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
cookie-es@1.2.2:
resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==}
cookie-es@1.2.3:
resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==}
cookie-es@3.0.1:
resolution: {integrity: sha512-1HqK6V6Prrm7cHzmjtCzfa370ljqPQoNeWIh5NXYqWwUI2l3DZlzjHHh25ZvJIPfwkiYnCvHykE2MIJkEVD1Rg==}
@@ -6258,8 +6277,8 @@ packages:
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
engines: {node: '>= 0.4'}
defu@6.1.4:
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
defu@6.1.7:
resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
@@ -6881,8 +6900,8 @@ packages:
resolution: {integrity: sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q==}
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
h3@1.15.1:
resolution: {integrity: sha512-+ORaOBttdUm1E2Uu/obAyCguiI7MbBvsLTndc3gyK3zU+SYLoZXlyCP9Xgy0gikkGufFLTZXCXD6+4BsufnmHA==}
h3@1.15.11:
resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==}
handlebars@4.7.8:
resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==}
@@ -7803,8 +7822,8 @@ packages:
peerDependencies:
webpack: ^5.0.0
node-mock-http@1.0.2:
resolution: {integrity: sha512-zWaamgDUdo9SSLw47we78+zYw/bDr5gH8pH7oRRs8V3KmBtu8GLgGIbV2p/gRPd3LWpEOpjQj7X1FOU3VFMJ8g==}
node-mock-http@1.0.4:
resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==}
node-releases@2.0.46:
resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==}
@@ -9279,12 +9298,12 @@ packages:
trough@2.2.0:
resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
trpc-to-openapi@3.2.0:
resolution: {integrity: sha512-nRexppGtL7og73dI4900xnfiGoOAx9PppAi7CVNeh8JfRHXUJ3lDe4JCjHgDAvC3FPHmwJAGDJ0b0BbRll98YA==}
trpc-to-openapi@3.3.0:
resolution: {integrity: sha512-PNMXq0K37Ott5ysNACYN1XLaM5xZGNJrx1QiQVTlk4hmgJomoyiAKhVrpwrK02JW3joSQmHYBOZ7pK6Hdioekw==}
peerDependencies:
'@trpc/server': ^11.1.0
zod: ^4.0.0
zod-openapi: ^5.0.1
zod: ^3.25.0 || ^4.0.0
zod-openapi: ^5.4.4
ts-mixer@6.0.4:
resolution: {integrity: sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==}
@@ -12161,6 +12180,14 @@ snapshots:
'@tabler/icons@3.44.0': {}
'@tanstack/devtools-event-client@0.4.0': {}
'@tanstack/form-core@1.27.7':
dependencies:
'@tanstack/devtools-event-client': 0.4.0
'@tanstack/pacer-lite': 0.1.1
'@tanstack/store': 0.7.7
'@tanstack/match-sorter-utils@8.19.4':
dependencies:
remove-accents: 0.5.0
@@ -12198,6 +12225,10 @@ snapshots:
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
'@tanstack/store@0.7.7': {}
'@tanstack/store@0.8.0': {}
'@tanstack/table-core@8.20.5': {}
'@tanstack/virtual-core@3.11.2': {}
@@ -13455,7 +13486,7 @@ snapshots:
convert-source-map@2.0.0: {}
cookie-es@1.2.2: {}
cookie-es@1.2.3: {}
cookie-es@3.0.1: {}
@@ -13612,7 +13643,7 @@ snapshots:
es-errors: 1.3.0
gopd: 1.2.0
defu@6.1.4: {}
defu@6.1.7: {}
delayed-stream@1.0.0: {}
@@ -14233,14 +14264,14 @@ snapshots:
graphql@16.14.0:
optional: true
h3@1.15.1:
h3@1.15.11:
dependencies:
cookie-es: 1.2.2
cookie-es: 1.2.3
crossws: 0.3.5
defu: 6.1.4
defu: 6.1.7
destr: 2.0.5
iron-webcrypto: 1.2.1
node-mock-http: 1.0.2
node-mock-http: 1.0.4
radix3: 1.1.2
ufo: 1.6.4
uncrypto: 0.1.3
@@ -15264,7 +15295,7 @@ snapshots:
loader-utils: 2.0.4
webpack: 5.107.1(postcss@8.5.8)
node-mock-http@1.0.2: {}
node-mock-http@1.0.4: {}
node-releases@2.0.46: {}
@@ -16889,11 +16920,11 @@ snapshots:
trough@2.2.0: {}
trpc-to-openapi@3.2.0(patch_hash=2ca3c16af0fcca0c736697ad4fe553a14f794524fa9ce0d5c3e8ee4aea76090c)(@trpc/server@11.17.0(typescript@5.9.3))(zod-openapi@5.3.0(zod@4.4.3))(zod@4.4.3):
trpc-to-openapi@3.3.0(patch_hash=2ca3c16af0fcca0c736697ad4fe553a14f794524fa9ce0d5c3e8ee4aea76090c)(@trpc/server@11.17.0(typescript@5.9.3))(zod-openapi@5.3.0(zod@4.4.3))(zod@4.4.3):
dependencies:
'@trpc/server': 11.17.0(typescript@5.9.3)
co-body: 6.2.0
h3: 1.15.1
h3: 1.15.11
openapi3-ts: 4.4.0
zod: 4.4.3
zod-openapi: 5.3.0(zod@4.4.3)
+1 -1
View File
@@ -160,7 +160,7 @@ catalog:
swagger-ui-react: ^5.32.6
testcontainers: ^12.0.0
timezones-ical-library: ^2.2.0
trpc-to-openapi: ^3.2.0
trpc-to-openapi: ^3.3.0
tsdav: ^2.2.2
tsx: 4.20.4
turbo: ^2.9.14
+1 -1
View File
@@ -1 +1 @@
[{"login":"ajnart","avatar_url":"https://avatars.githubusercontent.com/u/49837342?v=4"},{"login":"manuel-rw","avatar_url":"https://avatars.githubusercontent.com/u/30572287?v=4"},{"login":"Meierschlumpf","avatar_url":"https://avatars.githubusercontent.com/u/63781622?v=4"},{"login":"SeDemal","avatar_url":"https://avatars.githubusercontent.com/u/26098587?v=4"},{"login":"lammersbjorn","avatar_url":"https://avatars.githubusercontent.com/u/71191962?v=4"},{"login":"LarveyOfficial","avatar_url":"https://avatars.githubusercontent.com/u/39219859?v=4"},{"login":"c00ldude1oo","avatar_url":"https://avatars.githubusercontent.com/u/15677803?v=4"},{"login":"InterN0te","avatar_url":"https://avatars.githubusercontent.com/u/10882916?v=4"},{"login":"Aimsucks","avatar_url":"https://avatars.githubusercontent.com/u/1828486?v=4"},{"login":"JannesV","avatar_url":"https://avatars.githubusercontent.com/u/5984244?v=4"},{"login":"MauriceNino","avatar_url":"https://avatars.githubusercontent.com/u/18658092?v=4"},{"login":"hillaliy","avatar_url":"https://avatars.githubusercontent.com/u/950010?v=4"},{"login":"Skeeww","avatar_url":"https://avatars.githubusercontent.com/u/9639992?v=4"},{"login":"RichyHBM","avatar_url":"https://avatars.githubusercontent.com/u/5472275?v=4"},{"login":"Bonfire","avatar_url":"https://avatars.githubusercontent.com/u/5704760?v=4"},{"login":"wieli99","avatar_url":"https://avatars.githubusercontent.com/u/46794825?v=4"},{"login":"ishaanparlikar","avatar_url":"https://avatars.githubusercontent.com/u/43013132?v=4"},{"login":"Zareix","avatar_url":"https://avatars.githubusercontent.com/u/29818713?v=4"},{"login":"tuggan","avatar_url":"https://avatars.githubusercontent.com/u/4325608?v=4"},{"login":"Spillebulle","avatar_url":"https://avatars.githubusercontent.com/u/46653946?v=4"},{"login":"spkesDE","avatar_url":"https://avatars.githubusercontent.com/u/1682314?v=4"},{"login":"j3lte","avatar_url":"https://avatars.githubusercontent.com/u/2557568?v=4"},{"login":"dslatt","avatar_url":"https://avatars.githubusercontent.com/u/5211001?v=4"},{"login":"Malong11-007","avatar_url":"https://avatars.githubusercontent.com/u/40298510?v=4"},{"login":"StefanB7","avatar_url":"https://avatars.githubusercontent.com/u/22056509?v=4"},{"login":"souravghosh01","avatar_url":"https://avatars.githubusercontent.com/u/37548139?v=4"},{"login":"SR3u","avatar_url":"https://avatars.githubusercontent.com/u/2380522?v=4"},{"login":"Rikpat","avatar_url":"https://avatars.githubusercontent.com/u/33869814?v=4"},{"login":"NormanJS","avatar_url":"https://avatars.githubusercontent.com/u/83409549?v=4"},{"login":"No-Maines-Land","avatar_url":"https://avatars.githubusercontent.com/u/20687725?v=4"},{"login":"stark1tty","avatar_url":"https://avatars.githubusercontent.com/u/69326376?v=4"},{"login":"Momcilo42","avatar_url":"https://avatars.githubusercontent.com/u/10118137?v=4"},{"login":"marius-arch","avatar_url":"https://avatars.githubusercontent.com/u/72494359?v=4"},{"login":"Lumilias","avatar_url":"https://avatars.githubusercontent.com/u/10852161?v=4"},{"login":"Havock94","avatar_url":"https://avatars.githubusercontent.com/u/7635248?v=4"},{"login":"Lollyde","avatar_url":"https://avatars.githubusercontent.com/u/11347524?v=4"},{"login":"krishnamuppaneni","avatar_url":"https://avatars.githubusercontent.com/u/4703366?v=4"},{"login":"devtobi","avatar_url":"https://avatars.githubusercontent.com/u/28538704?v=4"},{"login":"TyxTang","avatar_url":"https://avatars.githubusercontent.com/u/90897955?v=4"},{"login":"alotlikebeans","avatar_url":"https://avatars.githubusercontent.com/u/19630511?v=4"},{"login":"WillyJL","avatar_url":"https://avatars.githubusercontent.com/u/49810075?v=4"},{"login":"anonysoul","avatar_url":"https://avatars.githubusercontent.com/u/105715150?v=4"},{"login":"cospeedster","avatar_url":"https://avatars.githubusercontent.com/u/52749645?v=4"},{"login":"diederbert","avatar_url":"https://avatars.githubusercontent.com/u/162878798?v=4"},{"login":"digdilem","avatar_url":"https://avatars.githubusercontent.com/u/4052275?v=4"},{"login":"dinghino","avatar_url":"https://avatars.githubusercontent.com/u/14729459?v=4"},{"login":"gfomichev","avatar_url":"https://avatars.githubusercontent.com/u/1596146?v=4"},{"login":"gnattu","avatar_url":"https://avatars.githubusercontent.com/u/12995396?v=4"},{"login":"tuxpizza","avatar_url":"https://avatars.githubusercontent.com/u/84710786?v=4"},{"login":"xmarkurion","avatar_url":"https://avatars.githubusercontent.com/u/46654687?v=4"},{"login":"huanga","avatar_url":"https://avatars.githubusercontent.com/u/732970?v=4"},{"login":"taos15","avatar_url":"https://avatars.githubusercontent.com/u/122130728?v=4"},{"login":"arghyadipchak","avatar_url":"https://avatars.githubusercontent.com/u/32430374?v=4"},{"login":"AuthorShin","avatar_url":"https://avatars.githubusercontent.com/u/4959043?v=4"},{"login":"benniblot","avatar_url":"https://avatars.githubusercontent.com/u/47495914?v=4"},{"login":"bebsworthy","avatar_url":"https://avatars.githubusercontent.cLine truncated
[{"login":"ajnart","avatar_url":"https://avatars.githubusercontent.com/u/49837342?v=4"},{"login":"manuel-rw","avatar_url":"https://avatars.githubusercontent.com/u/30572287?v=4"},{"login":"Meierschlumpf","avatar_url":"https://avatars.githubusercontent.com/u/63781622?v=4"},{"login":"SeDemal","avatar_url":"https://avatars.githubusercontent.com/u/26098587?v=4"},{"login":"lammersbjorn","avatar_url":"https://avatars.githubusercontent.com/u/71191962?v=4"},{"login":"LarveyOfficial","avatar_url":"https://avatars.githubusercontent.com/u/39219859?v=4"},{"login":"c00ldude1oo","avatar_url":"https://avatars.githubusercontent.com/u/15677803?v=4"},{"login":"InterN0te","avatar_url":"https://avatars.githubusercontent.com/u/10882916?v=4"},{"login":"Aimsucks","avatar_url":"https://avatars.githubusercontent.com/u/1828486?v=4"},{"login":"JannesV","avatar_url":"https://avatars.githubusercontent.com/u/5984244?v=4"},{"login":"MauriceNino","avatar_url":"https://avatars.githubusercontent.com/u/18658092?v=4"},{"login":"hillaliy","avatar_url":"https://avatars.githubusercontent.com/u/950010?v=4"},{"login":"Skeeww","avatar_url":"https://avatars.githubusercontent.com/u/9639992?v=4"},{"login":"RichyHBM","avatar_url":"https://avatars.githubusercontent.com/u/5472275?v=4"},{"login":"Bonfire","avatar_url":"https://avatars.githubusercontent.com/u/5704760?v=4"},{"login":"wieli99","avatar_url":"https://avatars.githubusercontent.com/u/46794825?v=4"},{"login":"ishaanparlikar","avatar_url":"https://avatars.githubusercontent.com/u/43013132?v=4"},{"login":"Zareix","avatar_url":"https://avatars.githubusercontent.com/u/29818713?v=4"},{"login":"tuggan","avatar_url":"https://avatars.githubusercontent.com/u/4325608?v=4"},{"login":"Spillebulle","avatar_url":"https://avatars.githubusercontent.com/u/46653946?v=4"},{"login":"marektancak0106","avatar_url":"https://avatars.githubusercontent.com/u/25422667?v=4"},{"login":"spkesDE","avatar_url":"https://avatars.githubusercontent.com/u/1682314?v=4"},{"login":"j3lte","avatar_url":"https://avatars.githubusercontent.com/u/2557568?v=4"},{"login":"dslatt","avatar_url":"https://avatars.githubusercontent.com/u/5211001?v=4"},{"login":"Malong11-007","avatar_url":"https://avatars.githubusercontent.com/u/40298510?v=4"},{"login":"StefanB7","avatar_url":"https://avatars.githubusercontent.com/u/22056509?v=4"},{"login":"souravghosh01","avatar_url":"https://avatars.githubusercontent.com/u/37548139?v=4"},{"login":"SR3u","avatar_url":"https://avatars.githubusercontent.com/u/2380522?v=4"},{"login":"Rikpat","avatar_url":"https://avatars.githubusercontent.com/u/33869814?v=4"},{"login":"NormanJS","avatar_url":"https://avatars.githubusercontent.com/u/83409549?v=4"},{"login":"No-Maines-Land","avatar_url":"https://avatars.githubusercontent.com/u/20687725?v=4"},{"login":"stark1tty","avatar_url":"https://avatars.githubusercontent.com/u/69326376?v=4"},{"login":"Momcilo42","avatar_url":"https://avatars.githubusercontent.com/u/10118137?v=4"},{"login":"marius-arch","avatar_url":"https://avatars.githubusercontent.com/u/72494359?v=4"},{"login":"Lumilias","avatar_url":"https://avatars.githubusercontent.com/u/10852161?v=4"},{"login":"Havock94","avatar_url":"https://avatars.githubusercontent.com/u/7635248?v=4"},{"login":"Lollyde","avatar_url":"https://avatars.githubusercontent.com/u/11347524?v=4"},{"login":"krishnamuppaneni","avatar_url":"https://avatars.githubusercontent.com/u/4703366?v=4"},{"login":"devtobi","avatar_url":"https://avatars.githubusercontent.com/u/28538704?v=4"},{"login":"TyxTang","avatar_url":"https://avatars.githubusercontent.com/u/90897955?v=4"},{"login":"alotlikebeans","avatar_url":"https://avatars.githubusercontent.com/u/19630511?v=4"},{"login":"WillyJL","avatar_url":"https://avatars.githubusercontent.com/u/49810075?v=4"},{"login":"anonysoul","avatar_url":"https://avatars.githubusercontent.com/u/105715150?v=4"},{"login":"cospeedster","avatar_url":"https://avatars.githubusercontent.com/u/52749645?v=4"},{"login":"diederbert","avatar_url":"https://avatars.githubusercontent.com/u/162878798?v=4"},{"login":"digdilem","avatar_url":"https://avatars.githubusercontent.com/u/4052275?v=4"},{"login":"dinghino","avatar_url":"https://avatars.githubusercontent.com/u/14729459?v=4"},{"login":"gfomichev","avatar_url":"https://avatars.githubusercontent.com/u/1596146?v=4"},{"login":"gnattu","avatar_url":"https://avatars.githubusercontent.com/u/12995396?v=4"},{"login":"tuxpizza","avatar_url":"https://avatars.githubusercontent.com/u/84710786?v=4"},{"login":"xmarkurion","avatar_url":"https://avatars.githubusercontent.com/u/46654687?v=4"},{"login":"huanga","avatar_url":"https://avatars.githubusercontent.com/u/732970?v=4"},{"login":"taos15","avatar_url":"https://avatars.githubusercontent.com/u/122130728?v=4"},{"login":"arghyadipchak","avatar_url":"https://avatars.githubusercontent.com/u/32430374?v=4"},{"login":"AuthorShin","avatar_url":"https://avatars.githubusercontent.com/u/4959043?v=4"},{"login":"benniblot","avatar_url":"https://avatars.githubusercontLine truncated
+1 -1
View File
@@ -1 +1 @@
[{"username":"carlchina","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/6697/medium/d22bbe7797bbeb30dbdc73a5648d329a_default.png"},{"username":"zielmann","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/7795/medium/ad22b8b8d5eb33e4154d53a454c862fd_default.png"},{"username":"Thalyn","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12494028/medium/5faf5acc945a874ed116c4567edaa09f_default.png"},{"username":"magnushj","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12503488/medium/68f32e33ea7b891960055c9f831f7344_default.png"},{"username":"almontegil","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12580457/medium/f4136cacbdfdb4c28ae7f85dc5f840db_default.png"},{"username":"andibing","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12664938/medium/b8be63e4dcb2e791ced1ffc9e3a049a5.jpg"},{"username":"gronare","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12680911/medium/c6eb14d1504156f34a158e507f73aa3d_default.png"},{"username":"SmartPhoneLover","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12701640/medium/ec95486662ec875cda080e778c3ff702.jpg"},{"username":"marcelotk_15","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12773227/medium/ecf6bea16ab0c873c131df2154ee9718_default.png"},{"username":"S3OD177","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13051544/medium/11dd990837f61f7e8f7d36b041a7fddb_default.png"},{"username":"marcg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13153301/medium/ebcf60fbf27deb148ec40a659566c6ff_default.png"},{"username":"BeersTeddy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13185230/medium/2f1f4e1effe74a23422b195cbefb2a95_default.png"},{"username":"jamesmcmahon0","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13245578/medium/586aa873b4abddbd9abc6f3de99ab70e.jpeg"},{"username":"b1nar10","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13286752/medium/249b02e3368752cb175446082015737c_default.png"},{"username":"vannCN","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13330448/medium/9b8a9ee3611e51e951e22d5fd4eb7d8d.jpg"},{"username":"binswm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13343482/medium/45dde0e6097b9b72705d2eba9dbbc276_default.png"},{"username":"SkewRam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13496556/medium/188f5c2deb7938eda51eb786cc4539ca.jpeg"},{"username":"raphcatarino","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13547726/medium/b003511e67df13a4b4b5689488fa8099.jpg"},{"username":"shillos5","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13600557/medium/b17650b2b84f829d54fa548c44c1ab27.jpeg"},{"username":"wolong98","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13641407/medium/f4634edc58c7857a357e5293543c15cf.jpg"},{"username":"spair0039","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14012333/medium/72430e96027c09c19141cac38eae4617.png"},{"username":"MajorMarcin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14072183/medium/0a197e64bb92b6ea94cecbb607512c45.jpg"},{"username":"KosmoMoustache","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14103535/medium/51c438cdd9f86e8811c75f4869de479c.png"},{"username":"Soochaehwa","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14118689/medium/496a1ce63111547bf455a1e0a7ac75f1_default.png"},{"username":"garryfield","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14187035/medium/520c964706676045f6eeec1b08edc880.jpg"},{"username":"droidenko","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14444264/medium/4aa3a8e824f72bc9e5ec0d8de307095e.jpeg"},{"username":"maathis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14489950/medium/6eeddf13a757ee35f78f1763f94d95e1.jpeg"},{"username":"bukvam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14546502/medium/a819dc40a13c57395a17dc46e847d3ce.jpg"},{"username":"gm.cinalli","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14670666/medium/2d466a0fdbda40764526be86c97c0ab4.jpeg"},{"username":"antoine2tt","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14722148/medium/f88d926900862dd59007ea4b3419cb9d.png"},{"username":"cretzen","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14799754/medium/3ba5c1805ec20c000d5810c07985afe9.jpg"},{"username":"ktKongTong","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14817246/medium/97cfc3c028dbdaf85ebd1102da71e58c.jpeg"},{"username":"marinkaberg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14861042/medium/2d5c4e62613f03082f3e645fa92efd59.jpeg"},{"username":"djismgaming","avatarUrl":"httpsLine truncated
[{"username":"carlchina","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/6697/medium/d22bbe7797bbeb30dbdc73a5648d329a_default.png"},{"username":"zielmann","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/7795/medium/ad22b8b8d5eb33e4154d53a454c862fd_default.png"},{"username":"Thalyn","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12494028/medium/5faf5acc945a874ed116c4567edaa09f_default.png"},{"username":"magnushj","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12503488/medium/68f32e33ea7b891960055c9f831f7344_default.png"},{"username":"almontegil","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12580457/medium/f4136cacbdfdb4c28ae7f85dc5f840db_default.png"},{"username":"andibing","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12664938/medium/b8be63e4dcb2e791ced1ffc9e3a049a5.jpg"},{"username":"gronare","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12680911/medium/c6eb14d1504156f34a158e507f73aa3d_default.png"},{"username":"SmartPhoneLover","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12701640/medium/ec95486662ec875cda080e778c3ff702.jpg"},{"username":"marcelotk_15","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12773227/medium/ecf6bea16ab0c873c131df2154ee9718_default.png"},{"username":"S3OD177","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13051544/medium/11dd990837f61f7e8f7d36b041a7fddb_default.png"},{"username":"marcg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13153301/medium/ebcf60fbf27deb148ec40a659566c6ff_default.png"},{"username":"BeersTeddy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13185230/medium/2f1f4e1effe74a23422b195cbefb2a95_default.png"},{"username":"jamesmcmahon0","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13245578/medium/586aa873b4abddbd9abc6f3de99ab70e.jpeg"},{"username":"b1nar10","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13286752/medium/249b02e3368752cb175446082015737c_default.png"},{"username":"vannCN","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13330448/medium/9b8a9ee3611e51e951e22d5fd4eb7d8d.jpg"},{"username":"binswm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13343482/medium/45dde0e6097b9b72705d2eba9dbbc276_default.png"},{"username":"SkewRam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13496556/medium/188f5c2deb7938eda51eb786cc4539ca.jpeg"},{"username":"raphcatarino","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13547726/medium/b003511e67df13a4b4b5689488fa8099.jpg"},{"username":"shillos5","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13600557/medium/b17650b2b84f829d54fa548c44c1ab27.jpeg"},{"username":"wolong98","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13641407/medium/f4634edc58c7857a357e5293543c15cf.jpg"},{"username":"spair0039","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14012333/medium/72430e96027c09c19141cac38eae4617.png"},{"username":"MajorMarcin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14072183/medium/0a197e64bb92b6ea94cecbb607512c45.jpg"},{"username":"KosmoMoustache","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14103535/medium/51c438cdd9f86e8811c75f4869de479c.png"},{"username":"Soochaehwa","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14118689/medium/496a1ce63111547bf455a1e0a7ac75f1_default.png"},{"username":"garryfield","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14187035/medium/520c964706676045f6eeec1b08edc880.jpg"},{"username":"droidenko","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14444264/medium/4aa3a8e824f72bc9e5ec0d8de307095e.jpeg"},{"username":"maathis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14489950/medium/6eeddf13a757ee35f78f1763f94d95e1.jpeg"},{"username":"bukvam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14546502/medium/a819dc40a13c57395a17dc46e847d3ce.jpg"},{"username":"gm.cinalli","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14670666/medium/2d466a0fdbda40764526be86c97c0ab4.jpeg"},{"username":"antoine2tt","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14722148/medium/f88d926900862dd59007ea4b3419cb9d.png"},{"username":"cretzen","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14799754/medium/3ba5c1805ec20c000d5810c07985afe9.jpg"},{"username":"ktKongTong","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14817246/medium/97cfc3c028dbdaf85ebd1102da71e58c.jpeg"},{"username":"marinkaberg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14861042/medium/2d5c4e62613f03082f3e645fa92efd59.jpeg"},{"username":"djismgaming","avatarUrl":"httpsLine truncated