Merge origin/dev into feat/phase1-perf-impl

The branch was 12 commits behind dev and GitHub reported it CONFLICTING, which is
why the real CI workflow had not run since 2026-08-10 - a conflicting PR has no
mergeable ref, so pull_request-triggered workflows are skipped. Only the
lightweight checks (semantic title, CodeQL, CodeRabbit) were still reporting,
which made the PR look green while nothing was actually being tested.

Three conflicts, all of them this branch's barrel-diet work meeting dev's new
What's Up Docker integration. In every case the branch's structure is kept and
WUD is absorbed into it rather than the reverse:

  integrations/base/creator.ts   keep the lazy loader map; add wud as
                                () => import(...) rather than dev's static import,
                                which would have undone the lazy creator
  integrations/src/index.ts      keep the slim barrel; re-export WudStats and
                                WudContainerUpdate as `export type`, which is
                                erased at build and so adds no runtime module
  widgets/src/index.tsx          stays deleted - this was the barrel that put
                                every widget in the initial client graph

Registering WUD in the slimmed structures then needed three more entries that the
merge could not infer: base/kind-map.ts (type-only kind -> class map),
widgets/src/import.ts (type-only WidgetImports map, both the import and the
member). Each was found by typecheck rather than guessed at.

Verified: 37/37 packages typecheck, 1866/1881 unit tests pass. The 3 failures are
home-assistant and pi-hole, both testcontainers-based; both pass 11/11 in
isolation and fail only under 150-file parallel load with the container's payload
half-initialised.
This commit is contained in:
Thomas Camlong committed 2026-08-13 18:40:08 +02:00
commit 89f081538c
92 files changed
+3645 -2049

No files matched your search

@@ -1,38 +1,17 @@
"use client";
import type { ComponentProps, ReactNode } from "react";
import { Suspense, use, useEffect, useState } from "react";
import { Box, Loader, UnstyledButton, useMantineColorScheme } from "@mantine/core";
import type { ReactNode } from "react";
import { useEffect, useState } from "react";
import { UnstyledButton, useMantineColorScheme } from "@mantine/core";
import { useHotkeys } from "@mantine/hooks";
import { ErrorBoundary } from "react-error-boundary";
import { clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client";
import { hotkeys } from "@homarr/definitions";
import { UserAvatarMenu } from "~/components/user-avatar-menu";
import { UpdateIndicator } from "./update";
type UserAvatarMenuModule = typeof import("~/components/user-avatar-menu");
type UserAvatarMenuProps = ComponentProps<UserAvatarMenuModule["UserAvatarMenu"]>;
let userAvatarMenuPromise: Promise<UserAvatarMenuModule> | undefined;
const loadUserAvatarMenu = () => {
if (userAvatarMenuPromise) return userAvatarMenuPromise;
const promise = import("~/components/user-avatar-menu");
userAvatarMenuPromise = promise;
void promise.catch(() => {
if (userAvatarMenuPromise === promise) userAvatarMenuPromise = undefined;
});
return promise;
};
const preloadUserAvatarMenu = () => void loadUserAvatarMenu().catch(() => undefined);
const LazyUserAvatarMenu = (props: UserAvatarMenuProps) => {
const { UserAvatarMenu } = use(loadUserAvatarMenu());
return <UserAvatarMenu {...props} />;
};
interface UserButtonClientProps {
avatar: ReactNode;
isAdmin: boolean;
@@ -41,8 +20,6 @@ interface UserButtonClientProps {
export const UserButtonClient = ({ avatar, isAdmin, isDockerEnabled }: UserButtonClientProps) => {
const [canCheckForUpdates, setCanCheckForUpdates] = useState(false);
const [isMenuMounted, setIsMenuMounted] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const session = useSession();
const { toggleColorScheme } = useMantineColorScheme();
useHotkeys([[hotkeys.toggleColorScheme, toggleColorScheme]]);
@@ -66,60 +43,14 @@ export const UserButtonClient = ({ avatar, isAdmin, isDockerEnabled }: UserButto
staleTime: 60 * 60 * 1_000,
});
const visibleUpdates = isCurrentSessionAdmin ? availableUpdates : undefined;
const openMenu = () => {
setIsMenuMounted(true);
setIsMenuOpen(true);
};
const renderButton = (onClick: () => void, isLoading = false) => (
<UnstyledButton
onClick={onClick}
onFocus={preloadUserAvatarMenu}
onMouseEnter={preloadUserAvatarMenu}
onPointerDown={preloadUserAvatarMenu}
aria-busy={isLoading || undefined}
disabled={isLoading}
>
<UpdateIndicator availableUpdates={visibleUpdates} disabled={!isCurrentSessionAdmin}>
<Box pos="relative" style={{ display: "inline-flex" }}>
<Box opacity={isLoading ? 0.35 : 1}>{avatar}</Box>
{isLoading && (
<Loader
aria-label="Loading user menu"
size={18}
pos="absolute"
top="50%"
left="50%"
style={{ transform: "translate(-50%, -50%)" }}
/>
)}
</Box>
</UpdateIndicator>
</UnstyledButton>
);
const button = renderButton(openMenu);
const loadingButton = renderButton(() => undefined, true);
if (!isMenuMounted) return button;
return (
<ErrorBoundary
fallbackRender={({ resetErrorBoundary }) =>
renderButton(() => {
resetErrorBoundary();
openMenu();
})
}
>
<Suspense fallback={loadingButton}>
<LazyUserAvatarMenu
availableUpdates={visibleUpdates}
isDockerEnabled={isDockerEnabled}
opened={isMenuOpen}
onOpenChange={setIsMenuOpen}
>
{button}
</LazyUserAvatarMenu>
</Suspense>
</ErrorBoundary>
<UserAvatarMenu availableUpdates={visibleUpdates} isDockerEnabled={isDockerEnabled}>
<UnstyledButton>
<UpdateIndicator availableUpdates={visibleUpdates} disabled={!isCurrentSessionAdmin}>
{avatar}
</UpdateIndicator>
</UnstyledButton>
</UserAvatarMenu>
);
};
@@ -1,19 +1,21 @@
"use client";
import { Fragment } from "react";
import { Card, CardSection, Divider, Group, Stack, Text, Title } from "@mantine/core";
import { getI18n } from "@homarr/translation/server";
import { useScopedI18n } from "@homarr/translation/client";
interface DangerZoneRootProps {
children: React.ReactNode[] | React.ReactNode;
}
export const DangerZoneRoot = async ({ children }: DangerZoneRootProps) => {
const t = await getI18n();
export const DangerZoneRoot = ({ children }: DangerZoneRootProps) => {
const t = useScopedI18n("common");
return (
<Stack gap="sm">
<Title c="red.8" order={2}>
{t("common.dangerZone")}
{t("dangerZone")}
</Title>
<Card style={{ borderColor: "var(--mantine-color-red-8)", borderWidth: 3 }}>
<Stack gap="sm">
@@ -0,0 +1,19 @@
import type { ReactNode } from "react";
import { Card, Divider, Stack, Title } from "@mantine/core";
interface SectionCardProps {
title: ReactNode;
children: ReactNode;
}
export const SectionCard = ({ title, children }: SectionCardProps) => {
return (
<Card withBorder bg="transparent">
<Stack gap="md">
<Title order={3}>{title}</Title>
<Divider />
{children}
</Stack>
</Card>
);
};
@@ -0,0 +1,25 @@
"use client";
import type { ReactNode } from "react";
import { Card, Group, Text } from "@mantine/core";
import { useI18n } from "@homarr/translation/client";
interface UnsavedChangesBarProps {
children: ReactNode;
}
export const UnsavedChangesBar = ({ children }: UnsavedChangesBarProps) => {
const t = useI18n();
return (
<div style={{ position: "sticky", bottom: 20 }}>
<Card withBorder>
<Group justify="space-between" wrap="wrap">
<Text fw={500}>{t("common.unsavedChanges")}</Text>
<Group>{children}</Group>
</Group>
</Card>
</div>
);
};
@@ -21,17 +21,9 @@ interface UserAvatarMenuProps {
children: ReactNode;
availableUpdates?: RouterOutputs["updateChecker"]["getAvailableUpdates"];
isDockerEnabled?: boolean;
opened: boolean;
onOpenChange: (opened: boolean) => void;
}
export const UserAvatarMenu = ({
children,
availableUpdates,
isDockerEnabled,
opened,
onOpenChange,
}: UserAvatarMenuProps) => {
export const UserAvatarMenu = ({ children, availableUpdates, isDockerEnabled }: UserAvatarMenuProps) => {
const t = useScopedI18n("common.userAvatar.menu");
const session = useSession();
@@ -48,7 +40,7 @@ export const UserAvatarMenu = ({
return (
// We use keepMounted so we can add event listeners to prevent navigating away without saving the board
<Menu width={300} withinPortal keepMounted opened={opened} onChange={onOpenChange}>
<Menu width={300} withinPortal keepMounted>
<Menu.Dropdown>
<AvailableUpdatesMenuItem availableUpdates={availableUpdates} />
<Menu.Item component={Link} href="/boards" leftSection={<IconHome size="1rem" />}>