Compare commits

..
Author SHA1 Message Date
Releases Homarr 46ae756d15 chore(release): 1.68.1 [skip ci] 2026-06-30 12:02:44 +00:00
Thomas 8b63799f1b Merge pull request #6124 from homarr-labs/fix/source-maps-memory-regression
fix: make production source maps opt-in to restore v1.67.0 memory baseline
2026-06-30 14:01:42 +02:00
ajnart 60a69e31ef fix: revert production source maps to restore v1.67.0 memory baseline
CodeRabbit review addressed:
- Removed the env-var gating (simpler: just remove source maps entirely)
- CI health check now fails fast if container never becomes healthy

Changes vs origin/main:
- next.config.ts: remove productionBrowserSourceMaps and serverSourceMaps
  (they default to false, restoring pre-v1.68.0 behavior)
- Dockerfile: remove .map copy step and NODE_OPTIONS=--enable-source-maps
- CI: add memory-regression job (<700MB guardrail)

Closes #6123
2026-06-29 15:13:08 +02:00
ajnart 57ef0af4a2 fix: make source maps opt-in to restore v1.67.0 memory baseline
Production source maps (--enable-source-maps) caused a ~50% memory increase
(600MB to 1000MB) in v1.68.0. Node.js loads all .map files into memory when
this flag is enabled, and Next.js server chunks generate hundreds of map files.

Changes:
- Guard productionBrowserSourceMaps and serverSourceMaps behind
  HOMARR_ENABLE_SOURCE_MAPS env var (default off)
- Guard .map copy step and NODE_OPTIONS in Dockerfile behind
  ENABLE_SOURCE_MAPS build arg (default false)
- run.sh checks HOMARR_ENABLE_SOURCE_MAPS before setting --enable-source-maps
- Add Docker-based memory regression CI job (<700MB threshold)

Closes #6123
2026-06-29 10:08:52 +02:00
Thomas 597b5fbc8d Merge pull request #6097 from homarr-labs/docs/update-helm-docs
Update Helm chart documentation
2026-06-27 13:24:44 +02:00
github-actions[bot] 5412577519 docs(helm): Update Helm chart documentation 2026-06-27 11:22:16 +00:00
25 changed files with 738 additions and 945 deletions

No files matched your search

-1
View File
@@ -33,7 +33,6 @@ body:
options:
# The below comment is used to insert a new version with on-release.yml
#NEXT_VERSION#
- 1.68.0
- 1.67.0
- 1.66.1
- 1.66.0
+62
View File
@@ -163,3 +163,65 @@ jobs:
run: cp .env.example .env
- name: Build
run: pnpm turbo build --affected
memory-regression:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Build Docker image
uses: docker/build-push-action@v7
with:
platforms: linux/amd64
context: .
push: false
load: true
tags: homarr-memory-test
network: host
cache-from: type=gha
cache-to: type=gha,mode=max
env:
SKIP_ENV_VALIDATION: true
- name: Start container and measure memory
run: |
docker run -d --name homarr-mem-test -p 17575:7575 homarr-memory-test
# Wait for container to be healthy (up to 60s)
healthy=false
for i in $(seq 1 30); do
if curl -sf http://localhost:17575/api/health 2>/dev/null; then
echo "Container healthy after $((i * 2))s"
healthy=true
break
fi
sleep 2
done
if [ "$healthy" != "true" ]; then
echo "ERROR: Container never became healthy"
docker logs homarr-mem-test --tail 50
exit 1
fi
# Warm up: hit the dashboard page a few times
for i in $(seq 1 5); do
curl -sf http://localhost:17575/ > /dev/null 2>&1 || true
sleep 2
done
# Measure RSS after warmup
MEM_KB=$(docker stats --no-stream --format '{{.MemUsage}}' homarr-mem-test | sed 's| /.*||')
echo "Raw memory: $MEM_KB"
# Convert to numeric MB (handles GiB and MiB suffixes)
MEM_NUM=$(echo "$MEM_KB" | awk '{
if ($0 ~ /GiB/) { gsub(/GiB/,""); print $1 * 1024 }
else if ($0 ~ /MiB/) { gsub(/MiB/,""); print $1 }
else if ($0 ~ /KiB/) { gsub(/KiB/,""); print $1 / 1024 }
else { print $1 / 1048576 }
}')
echo "Memory usage: ${MEM_NUM} MB"
MAX_MB=700
if awk "BEGIN {exit !($MEM_NUM > $MAX_MB)}"; then
echo "ERROR: Memory usage ${MEM_NUM} MB exceeds ${MAX_MB} MB threshold"
docker logs homarr-mem-test --tail 50
exit 1
fi
echo "Memory check passed: ${MEM_NUM} MB <= ${MAX_MB} MB"
+7
View File
@@ -1,3 +1,10 @@
## [1.68.1](https://github.com/homarr-labs/homarr/compare/v1.68.0...v1.68.1) (2026-06-30)
### Bug Fixes
* make source maps opt-in to restore v1.67.0 memory baseline ([57ef0af](https://github.com/homarr-labs/homarr/commit/57ef0af4a252ea39727caeba7e13587dabc6254e)), closes [#6123](https://github.com/homarr-labs/homarr/issues/6123)
* revert production source maps to restore v1.67.0 memory baseline ([60a69e3](https://github.com/homarr-labs/homarr/commit/60a69e31efc7168b8f4309daf75954d7730d883b)), closes [#6123](https://github.com/homarr-labs/homarr/issues/6123)
## [1.68.0](https://github.com/homarr-labs/homarr/compare/v1.67.0...v1.68.0) (2026-06-26)
### Features
-14
View File
@@ -20,17 +20,6 @@ ARG DISABLE_REDIS_LOGS='true'
RUN corepack enable pnpm && pnpm build
# The standalone output tracing omits server chunk source maps (.next/server/**/*.map), which is
# where production Node stack frames point. Copy the .map files into the standalone tree (preserving
# directory structure) so the existing standalone COPY ships them, keeping traces readable.
# See https://github.com/homarr-labs/homarr/issues/5891
RUN cd apps/nextjs/.next/server && \
find . -name '*.map' | while IFS= read -r map; do \
dest="../standalone/apps/nextjs/.next/server/$map"; \
mkdir -p "$(dirname "$dest")"; \
cp "$map" "$dest"; \
done
FROM base AS runner
WORKDIR /app
@@ -75,9 +64,6 @@ ENV DB_DRIVER='better-sqlite3'
ENV AUTH_PROVIDERS='credentials'
ENV REDIS_IS_EXTERNAL='false'
ENV NODE_ENV='production'
# Tell Node to symbolicate stack traces using the shipped .map files so production logs
# show original source locations instead of minified frames, see https://github.com/homarr-labs/homarr/issues/5891
ENV NODE_OPTIONS='--enable-source-maps'
ENTRYPOINT [ "/app/entrypoint.sh" ]
CMD ["sh", "run.sh"]
@@ -316,7 +316,6 @@ You can drag and drop items in to this category in the edit mode.
Categories will have a big space before and after them in the edit mode to
enable you to drag and drop items outside the category.
You can also collapse or open categories in edit mode to facilitate your editing process.
Use the category menu to open every app in that category in new tabs, or use the board header action to open every app on the current board layout in new tabs.
:::note
@@ -2,9 +2,9 @@
<img src="https://raw.githubusercontent.com/homarr-labs/charts/refs/heads/main/charts/homarr/icon.svg" align="right" width="92" alt="homarr logo">
![Version: 8.19.0](https://img.shields.io/badge/Version-8.19.0-informational?style=flat)
![Version: 8.20.0](https://img.shields.io/badge/Version-8.20.0-informational?style=flat)
![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat)
![AppVersion: v1.60.0](https://img.shields.io/badge/AppVersion-v1.60.0-informational?style=flat)
![AppVersion: v1.68.0](https://img.shields.io/badge/AppVersion-v1.68.0-informational?style=flat)
A Helm chart to deploy homarr for Kubernetes
@@ -383,6 +383,7 @@ All available values are listed on the [artifacthub](https://artifacthub.io/pack
| containerPorts | object | `{"http":{"port":7575,"protocol":"TCP"}}` | containerPorts defines the ports to open on the container. It is a map where each entry specifies: - `port` (int) (required): The port number to expose inside the container. - `protocol` (string) (required): The network protocol (TCP or UDP) used for the port. - `disabled` (bool) : Optional flag to disable this port (defaults to false). Can be overridden via Helm values. By default, this configuration exposes TCP port 7575 with the name `http`. |
| database.migrationEnabled | bool | `true` | Database migration configuration. DB_MIGRATIONS_DISABLED Set to `true` to disable database migrations. Migrations are enabled by default (`false`). |
| database.type | string | `"sqlite"` | Database type: sqlite, mysql or postgresql |
| env.AUTH_COOKIE_PREFIX | string | `"homarr"` | Prefix used for all authentication cookies. Change if you run another Auth.js/NextAuth app on the same hostname to avoid cookie name collisions. Only letters, numbers, hyphens and underscores. |
| env.AUTH_LDAP_BASE | string | `nil` | Base dn of your LDAP server |
| env.AUTH_LDAP_BIND_DN | string | `nil` | User used for finding users and groups |
| env.AUTH_LDAP_GROUP_CLASS | string | `"groupOfUniqueNames"` | Class used for querying groups |
@@ -395,7 +396,6 @@ All available values are listed on the [artifacthub](https://artifacthub.io/pack
| env.AUTH_LDAP_USERNAME_FILTER_EXTRA_ARG | string | `nil` | Extra arguments for user search filter (& based) |
| env.AUTH_LDAP_USER_MAIL_ATTRIBUTE | string | `"mail"` | Attribute used for mail field |
| env.AUTH_LOGOUT_REDIRECT_URL | string | `nil` | URL to redirect to after clicking logging out. |
| env.AUTH_COOKIE_PREFIX | string | `"homarr"` | Prefix used for all authentication cookies. Change if you run another Auth.js/NextAuth app on the same hostname to avoid cookie name collisions. Only letters, numbers, hyphens and underscores. |
| env.AUTH_OIDC_AUTO_LOGIN | string | `"false"` | Automatically redirect to OIDC login |
| env.AUTH_OIDC_CLIENT_NAME | string | `"OIDC"` | Display name of provider (in login screen) |
| env.AUTH_OIDC_GROUPS_ATTRIBUTE | string | `"groups"` | Attribute used for groups (roles) claim |
+1 -3
View File
@@ -63,9 +63,7 @@ import withoutIntegrations from './img/without-integrations.png';
### Interactions
- Hover over a marked date to preview its events in a popover.
- Click on a marked date to pin the popover open (it will stay visible even after moving the mouse away).
- Click the date again or click outside to close the pinned popover.
- View calendar events by clicking on the marked dates.
### Adding the widget
-4
View File
@@ -31,8 +31,6 @@ const nextConfig: NextConfig = {
},
output: "standalone",
reactStrictMode: true,
// Ship browser source maps so production client stack traces are readable, see https://github.com/homarr-labs/homarr/issues/5891
productionBrowserSourceMaps: true,
// react compiler breaks mantine-react-table, so disabled for now
//reactCompiler: true,
/** We already do typechecking as separate tasks in CI */
@@ -43,8 +41,6 @@ const nextConfig: NextConfig = {
*/
serverExternalPackages: ["dockerode", "isomorphic-dompurify", "jsdom", "better-sqlite3"],
experimental: {
// Ship server source maps so production Node stack traces in logs reference original source, see https://github.com/homarr-labs/homarr/issues/5891
serverSourceMaps: true,
optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"],
turbopackFileSystemCacheForDev: true,
preloadEntriesOnStart: false,
@@ -9,7 +9,6 @@ import {
IconBox,
IconBoxAlignTop,
IconChevronDown,
IconExternalLink,
IconLayoutBoard,
IconPencil,
IconPencilOff,
@@ -22,11 +21,8 @@ import {
import { clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client";
import { getCurrentLayout, useRequiredBoard } from "@homarr/boards/context";
import { useRequiredBoard } from "@homarr/boards/context";
import { useEditMode } from "@homarr/boards/edit-mode";
import { useSettings } from "@homarr/settings";
import { openAppsInNewTabs } from "~/components/board/open-apps-in-new-tabs";
import { filterByItemKind } from "~/components/board/sections/category/filter";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { env } from "@homarr/common/env";
import { hotkeys } from "@homarr/definitions";
@@ -50,34 +46,9 @@ export const BoardContentHeaderActions = () => {
const board = useRequiredBoard();
const { hasChangeAccess } = useBoardPermissions(board);
const { data: demoMode, isLoading } = clientApi.info.isDemoMode.useQuery();
const settings = useSettings();
const t = useI18n();
const { openConfirmModal } = useConfirmModal();
const openAllInNewTabs = useCallback(async () => {
const currentLayoutId = getCurrentLayout(board);
const appIds = filterByItemKind(
board.items.filter((item) => item.layouts.some((layout) => layout.layoutId === currentLayoutId)),
settings,
"app",
).map((item) => {
return item.options.appId;
});
await openAppsInNewTabs(appIds, { t, openConfirmModal });
}, [board, t, openConfirmModal, settings]);
if (!hasChangeAccess || isLoading) {
return (
<>
{!isLoading && (
<HeaderButton onClick={openAllInNewTabs} aria-label={t("section.category.action.openAllInNewTabs")}>
<IconExternalLink stroke={1.5} />
</HeaderButton>
)}
<SelectBoardsMenu />
</>
);
return <SelectBoardsMenu />;
}
return (
@@ -86,12 +57,6 @@ export const BoardContentHeaderActions = () => {
<EditModeMenu demoMode={demoMode ?? false} />
{!isEditMode && (
<HeaderButton onClick={openAllInNewTabs} aria-label={t("section.category.action.openAllInNewTabs")}>
<IconExternalLink stroke={1.5} />
</HeaderButton>
)}
{!demoMode && (
<OnboardingTour.Target id="board-settings">
<HeaderButton href={`/boards/${board.name}/settings`}>
@@ -1,48 +0,0 @@
import { fetchApi } from "@homarr/api/client";
type AppWithHref = {
href: string | null;
};
type TranslationKey = "section.category.openAllInNewTabs.title" | "section.category.openAllInNewTabs.text";
interface OpenAppsInNewTabsOptions {
t: (key: TranslationKey) => string;
openConfirmModal: (options: { title: string; children: string }) => void;
fetchAppsByIds?: (appIds: string[]) => Promise<AppWithHref[]>;
}
export const openAppsInNewTabs = async (
appIds: string[],
{ t, openConfirmModal, fetchAppsByIds = fetchApi.app.byIds.query }: OpenAppsInNewTabsOptions,
) => {
if (appIds.length === 0) return;
const distinctAppIds = [...new Set(appIds)];
const openedWindows = distinctAppIds.map(() => window.open("", "_blank", "noopener,noreferrer"));
for (const openedWindow of openedWindows) {
if (!openedWindow) continue;
openedWindow.opener = null;
}
const apps = await fetchAppsByIds(distinctAppIds);
const appsWithUrls = apps.filter((app) => app.href && app.href.length > 0);
const tabsToClose = openedWindows.slice(appsWithUrls.length).filter((openedWindow): openedWindow is Window => Boolean(openedWindow));
for (const tabToClose of tabsToClose) {
tabToClose.close();
}
for (const [index, app] of appsWithUrls.entries()) {
const openedWindow = openedWindows[index];
if (openedWindow) {
openedWindow.location.href = app.href;
continue;
}
openConfirmModal({
title: t("section.category.openAllInNewTabs.title"),
children: t("section.category.openAllInNewTabs.text"),
});
break;
}
};
@@ -1,5 +1,6 @@
import { useCallback } from "react";
import { fetchApi } from "@homarr/api/client";
import { getCurrentLayout, useRequiredBoard } from "@homarr/boards/context";
import { createId } from "@homarr/common";
import { useConfirmModal, useModalAction } from "@homarr/modals";
@@ -7,7 +8,6 @@ import { useSettings } from "@homarr/settings";
import { useI18n } from "@homarr/translation/client";
import type { CategorySection } from "~/app/[locale]/boards/_types";
import { openAppsInNewTabs } from "~/components/board/open-apps-in-new-tabs";
import { useCategoryActions } from "./category-actions";
import { CategoryEditModal } from "./category-edit-modal";
import { filterByItemKind } from "./filter";
@@ -115,7 +115,21 @@ export const useCategoryMenuActions = (category: CategorySection) => {
return item.options.appId;
});
await openAppsInNewTabs(appIds, { t, openConfirmModal });
const apps = await fetchApi.app.byIds.query(appIds);
const appsWithUrls = apps.filter((app) => app.href && app.href.length > 0);
for (const app of appsWithUrls) {
const openedWindow = window.open(app.href ?? undefined);
if (openedWindow) {
continue;
}
openConfirmModal({
title: t("section.category.openAllInNewTabs.title"),
children: t("section.category.openAllInNewTabs.text"),
});
break;
}
}, [category, board, t, openConfirmModal, settings]);
return {
@@ -1,65 +0,0 @@
import { describe, expect, test, vi } from "vitest";
import { openAppsInNewTabs } from "../open-apps-in-new-tabs";
describe("openAppsInNewTabs", () => {
test("opens tabs synchronously and navigates them to fetched URLs", async () => {
const openedWindowOne = { opener: null, location: { href: "" }, close: vi.fn() } as unknown as Window;
const openedWindowTwo = { opener: null, location: { href: "" }, close: vi.fn() } as unknown as Window;
const openedWindowThree = { opener: null, location: { href: "" }, close: vi.fn() } as unknown as Window;
const open = vi.spyOn(window, "open").mockReturnValueOnce(openedWindowOne).mockReturnValueOnce(openedWindowTwo).mockReturnValueOnce(openedWindowThree);
const fetchAppsByIds = vi
.fn()
.mockResolvedValue([{ href: "https://example.com/one" }, { href: null }, { href: "https://example.com/two" }]);
const openConfirmModal = vi.fn();
await openAppsInNewTabs(["app-one", "app-two", "app-three"], {
t: (key) => key,
openConfirmModal,
fetchAppsByIds,
});
expect(fetchAppsByIds).toHaveBeenCalledWith(["app-one", "app-two", "app-three"]);
expect(open).toHaveBeenCalledTimes(3);
expect(open).toHaveBeenNthCalledWith(1, "", "_blank", "noopener,noreferrer");
expect(open).toHaveBeenNthCalledWith(2, "", "_blank", "noopener,noreferrer");
expect(open).toHaveBeenNthCalledWith(3, "", "_blank", "noopener,noreferrer");
expect(openedWindowOne.location.href).toBe("https://example.com/one");
expect(openedWindowTwo.location.href).toBe("https://example.com/two");
expect(openedWindowThree.close).toHaveBeenCalledTimes(1);
expect(openConfirmModal).not.toHaveBeenCalled();
});
test("de-duplicates app IDs before fetching", async () => {
const openedWindow = { opener: null, location: { href: "" }, close: vi.fn() } as unknown as Window;
const open = vi.spyOn(window, "open").mockReturnValue(openedWindow);
const fetchAppsByIds = vi.fn().mockResolvedValue([{ href: "https://example.com/one" }]);
const openConfirmModal = vi.fn();
await openAppsInNewTabs(["app-one", "app-one", "app-one"], {
t: (key) => key,
openConfirmModal,
fetchAppsByIds,
});
expect(fetchAppsByIds).toHaveBeenCalledWith(["app-one"]);
expect(open).toHaveBeenCalledTimes(1);
});
test("shows confirm modal when popup is blocked", async () => {
vi.spyOn(window, "open").mockReturnValue(null);
const fetchAppsByIds = vi.fn().mockResolvedValue([{ href: "https://example.com/one" }]);
const openConfirmModal = vi.fn();
await openAppsInNewTabs(["app-one"], {
t: (key) => key,
openConfirmModal,
fetchAppsByIds,
});
expect(openConfirmModal).toHaveBeenCalledWith({
title: "section.category.openAllInNewTabs.title",
children: "section.category.openAllInNewTabs.text",
});
});
});
+2 -2
View File
@@ -6,7 +6,7 @@
},
"engines": {
"node": ">=24.17.0",
"pnpm": ">=11.9.0"
"pnpm": ">=11.8.0"
},
"packageManager": "pnpm@11.9.0"
"packageManager": "pnpm@11.8.0"
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "homarr",
"version": "1.68.0",
"version": "1.68.1",
"private": true,
"scripts": {
"build": "cross-env CI=true turbo build",
@@ -62,9 +62,9 @@
},
"engines": {
"node": ">=24.17.0",
"pnpm": ">=11.9.0"
"pnpm": ">=11.8.0"
},
"packageManager": "pnpm@11.9.0",
"packageManager": "pnpm@11.8.0",
"dependencies": {
"jsonpath-plus": "catalog:"
}
+1 -5
View File
@@ -2357,9 +2357,6 @@
"name": "Docker stats",
"description": "Stats of your containers (This widget can only be added with administrator privileges)",
"option": {
"columns": {
"label": "Columns to show"
},
"enableRowSorting": {
"label": "Enable items sorting"
},
@@ -5099,8 +5096,7 @@
}
},
"error": {
"internalServerError": "Failed to fetch Docker containers",
"noColumns": "Select at least one column in the widget settings"
"internalServerError": "Failed to fetch Docker containers"
}
},
"kubernetes": {
+7 -33
View File
@@ -1,4 +1,4 @@
import { useCallback, useState } from "react";
import { useState } from "react";
import { Box, Container, Flex, Popover, Text, useMantineTheme } from "@mantine/core";
import { useElementSize } from "@mantine/hooks";
@@ -13,12 +13,10 @@ interface CalendarDayProps {
disabled: boolean;
rootWidth: number;
rootHeight: number;
onOpen: (close: () => void) => void;
}
export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth, onOpen }: CalendarDayProps) => {
export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth }: CalendarDayProps) => {
const [opened, setOpened] = useState(false);
const [pinned, setPinned] = useState(false);
const { primaryColor } = useMantineTheme();
const { ref, height } = useElementSize();
const board = useRequiredBoard();
@@ -31,19 +29,6 @@ export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth, onO
const isTooSmallForIndicators = height < 30;
const handleMouseEnter = useCallback(() => {
if (disabled) return;
onOpen(() => {
setOpened(false);
setPinned(false);
});
setOpened(true);
}, [disabled, onOpen]);
const handleMouseLeave = useCallback(() => {
if (!pinned) setOpened(false);
}, [pinned]);
return (
<Popover
position="bottom"
@@ -54,10 +39,7 @@ export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth, onO
transitionProps={{
transition: "pop",
}}
onChange={(value) => {
setOpened(value);
if (!value) setPinned(false);
}}
onChange={setOpened}
opened={opened}
disabled={disabled}
>
@@ -77,10 +59,10 @@ export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth, onO
cursor: disabled ? "default" : "pointer",
}}
onClick={() => {
if (!disabled) setPinned((prev) => !prev);
if (disabled) return;
setOpened((prev) => !prev);
}}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<Text ta={"center"} size={shouldScaleDown ? "xs" : "md"} lh={1}>
{date.getDate()}
@@ -89,15 +71,7 @@ export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth, onO
</Container>
</Popover.Target>
{/* Popover has some offset on the left side, padding is removed because of scrollarea paddings */}
<Popover.Dropdown
maw="calc(100vw - 24px)"
w={512}
pe={4}
pb={0}
style={{ overflow: "hidden" }}
onMouseEnter={() => setOpened(true)}
onMouseLeave={handleMouseLeave}
>
<Popover.Dropdown maw="calc(100vw - 24px)" w={512} pe={4} pb={0} style={{ overflow: "hidden" }}>
<CalendarEventList events={events} />
</Popover.Dropdown>
</Popover>
+10 -50
View File
@@ -1,6 +1,6 @@
"use client";
import { useCallback, useMemo, useRef, useState } from "react";
import { useMemo, useState } from "react";
import { useParams } from "next/navigation";
import { useMantineTheme } from "@mantine/core";
import { Calendar } from "@mantine/dates";
@@ -16,15 +16,11 @@ import type { WidgetComponentProps } from "../definition";
import { CalendarDay } from "./calender-day";
import classes from "./component.module.css";
export default function CalendarWidget(
props: WidgetComponentProps<"calendar">,
) {
export default function CalendarWidget(props: WidgetComponentProps<"calendar">) {
const [month, setMonth] = useState(new Date());
if (props.integrationIds.length === 0) {
return (
<CalendarBase {...props} events={[]} month={month} setMonth={setMonth} />
);
return <CalendarBase {...props} events={[]} month={month} setMonth={setMonth} />;
}
return <FetchCalendar month={month} setMonth={setMonth} {...props} />;
@@ -35,13 +31,7 @@ interface FetchCalendarProps extends WidgetComponentProps<"calendar"> {
setMonth: (date: Date) => void;
}
const FetchCalendar = ({
month,
setMonth,
isEditMode,
integrationIds,
options,
}: FetchCalendarProps) => {
const FetchCalendar = ({ month, setMonth, isEditMode, integrationIds, options }: FetchCalendarProps) => {
const input = {
integrationIds,
month: month.getMonth(),
@@ -71,20 +61,9 @@ const FetchCalendar = ({
},
});
const events = useMemo(
() => data?.flatMap((item) => item.events) ?? [],
[data],
);
const events = useMemo(() => data?.flatMap((item) => item.events) ?? [], [data]);
return (
<CalendarBase
isEditMode={isEditMode}
events={events}
month={month}
setMonth={setMonth}
options={options}
/>
);
return <CalendarBase isEditMode={isEditMode} events={events} month={month} setMonth={setMonth} options={options} />;
};
interface CalendarBaseProps {
@@ -95,13 +74,7 @@ interface CalendarBaseProps {
options: WidgetComponentProps<"calendar">["options"];
}
const CalendarBase = ({
isEditMode,
events,
month,
setMonth,
options,
}: CalendarBaseProps) => {
const CalendarBase = ({ isEditMode, events, month, setMonth, options }: CalendarBaseProps) => {
const params = useParams();
const locale = params.locale as string;
const { firstDayOfWeek } = useSettings();
@@ -112,11 +85,6 @@ const CalendarBase = ({
const isSmall = width < 256;
const normalizedEvents = useMemo(() => splitEvents(events), [events]);
const activeCloseRef = useRef<(() => void) | null>(null);
const onDayOpen = useCallback((close: () => void) => {
activeCloseRef.current?.();
activeCloseRef.current = close;
}, []);
return (
<Calendar
@@ -182,14 +150,9 @@ const CalendarBase = ({
const eventsForDate = normalizedEvents
.filter((event) => dayjs(event.startDate).isSame(tileDate, "day"))
.filter(
(event) =>
event.metadata?.type !== "radarr" ||
options.releaseType.includes(event.metadata.releaseType),
(event) => event.metadata?.type !== "radarr" || options.releaseType.includes(event.metadata.releaseType),
)
.toSorted(
(eventA, eventB) =>
eventA.startDate.getTime() - eventB.startDate.getTime(),
);
.toSorted((eventA, eventB) => eventA.startDate.getTime() - eventB.startDate.getTime());
return (
<CalendarDay
@@ -198,7 +161,6 @@ const CalendarBase = ({
disabled={isEditMode || eventsForDate.length === 0}
rootWidth={width}
rootHeight={height}
onOpen={onDayOpen}
/>
);
}}
@@ -236,9 +198,7 @@ export const splitEvents = (events: CalendarEvent[]): CalendarEvent[] => {
splitEvents.push({
...event,
startDate: currentStart.toDate(),
endDate: currentStart.endOf("day").isAfter(event.endDate)
? event.endDate
: currentStart.endOf("day").toDate(),
endDate: currentStart.endOf("day").isAfter(event.endDate) ? event.endDate : currentStart.endOf("day").toDate(),
});
currentStart = currentStart.add(1, "day").startOf("day");
@@ -57,7 +57,6 @@ export default function DnsHoleControlsWidget({
},
{
staleTime: 5 * 1000,
refetchInterval: 5 * 1000,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
retry: false,
@@ -26,7 +26,6 @@ export default function DnsHoleSummaryWidget({ options, integrationIds }: Widget
},
{
staleTime: 5 * 1000,
refetchInterval: 5 * 1000,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
retry: false,
+2 -21
View File
@@ -1,10 +1,10 @@
"use client";
import { useMemo } from "react";
import { ActionIcon, Avatar, Badge, Center, Group, Stack, Text, Tooltip } from "@mantine/core";
import { ActionIcon, Avatar, Badge, Group, Stack, Text, Tooltip } from "@mantine/core";
import type { IconProps } from "@tabler/icons-react";
import { IconBrandDocker, IconPlayerPlay, IconPlayerStop, IconRotateClockwise } from "@tabler/icons-react";
import type { MRT_ColumnDef, MRT_VisibilityState } from "mantine-react-table";
import type { MRT_ColumnDef } from "mantine-react-table";
import { MantineReactTable } from "mantine-react-table";
import type { RouterOutputs } from "@homarr/api";
@@ -213,15 +213,6 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo
const columns = useMemo(() => createColumns(t), [t]);
const columnVisibility: MRT_VisibilityState = {
name: options.columns.includes("name"),
state: options.columns.includes("state"),
host: options.columns.includes("host"),
cpuUsage: options.columns.includes("cpuUsage"),
memoryUsage: options.columns.includes("memoryUsage"),
actions: options.columns.includes("actions"),
};
const table = useTranslatedMantineReactTable({
columns,
data: containers,
@@ -242,9 +233,6 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo
sorting: [{ id: options.defaultSort, desc: options.descendingDefaultSort }],
density: "xs",
},
state: {
columnVisibility,
},
mantinePaperProps: {
flex: 1,
withBorder: false,
@@ -272,13 +260,6 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo
},
});
if (options.columns.length === 0)
return (
<Center h="100%">
<Text>{t("error.noColumns")}</Text>
</Center>
);
return (
<Stack gap={0} h="100%" display="flex">
<MantineReactTable table={table} />
-19
View File
@@ -12,29 +12,10 @@ const columnsList = [
"memoryUsage",
] as const satisfies (keyof RouterOutputs["docker"]["getContainers"]["containers"][number])[];
const allColumnsList = ["name", "state", "host", "cpuUsage", "memoryUsage", "actions"] as const;
const columnTranslationKeyMap = {
name: "docker.field.name.label",
state: "docker.field.state.label",
host: "docker.field.host.label",
cpuUsage: "docker.field.stats.cpu.label",
memoryUsage: "docker.field.stats.memory.label",
actions: "docker.action.title",
} as const satisfies Record<(typeof allColumnsList)[number], string>;
export const { definition, componentLoader } = createWidgetDefinition("dockerContainers", {
icon: IconBrandDocker,
createOptions() {
return optionsBuilder.from((factory) => ({
columns: factory.multiSelect({
defaultValue: [...allColumnsList],
options: allColumnsList.map((value) => ({
value,
label: (t) => t(columnTranslationKeyMap[value]),
})),
searchable: true,
}),
enableRowSorting: factory.switch({
defaultValue: false,
}),
+1 -2
View File
@@ -1,7 +1,6 @@
"use client";
import "../widgets-common.css";
import "./styles.css";
import { useCallback, useMemo, useState } from "react";
import type { MantineStyleProp } from "@mantine/core";
@@ -578,7 +577,7 @@ export default function DownloadClientsWidget({
enableColumnOrdering: isEditMode,
enableRowVirtualization: true,
rowVirtualizerOptions: { overscan: 5 },
mantinePaperProps: { flex: 1, withBorder: false, shadow: undefined, bg: "transparent" },
mantinePaperProps: { flex: 1, withBorder: false, shadow: undefined },
mantineTableContainerProps: { style: { height: "100%" } },
mantineTableProps: {
className: "downloads-widget-table",
-12
View File
@@ -1,12 +0,0 @@
/* Make the MantineReactTable transparent so the board card's opacity-controlled
background shows through. MRT defaults to var(--mantine-color-body) which is opaque. */
.downloads-widget-table,
.downloads-widget-table .mantine-Table-table,
.downloads-widget-table .mantine-Table-thead,
.downloads-widget-table .mantine-Table-tbody,
.downloads-widget-table .mantine-Table-tfoot,
.downloads-widget-table .mantine-Table-tr,
.downloads-widget-table th,
.downloads-widget-table td {
background-color: transparent !important;
}
+598 -595
View File
File diff suppressed because it is too large. Load diff
+23 -23
View File
@@ -14,7 +14,7 @@ catalog:
"@dnd-kit/utilities": ^3.2.2
"@drizzle-team/brocli": ^0.12.0
"@extractus/feed-extractor": 7.2.1
'@gfazioli/mantine-onboarding-tour': ^4.0.6
'@gfazioli/mantine-onboarding-tour': ^4.0.5
"@gitbeaker/rest": ^43.8.0
"@homarr/gridstack": ^1.12.0
"@homarr/node-unifi": ^2.6.0
@@ -22,21 +22,21 @@ catalog:
"@jellyfin/sdk": ^0.13.0
"@kubernetes/client-node": ^1.4.0
"@modelcontextprotocol/sdk": 1.29.0
"@mantine/charts": ^9.4.0
"@mantine/colors-generator": ^9.4.0
"@mantine/core": ^9.4.0
"@mantine/dates": ^9.4.0
"@mantine/dropzone": ^9.4.0
"@mantine/form": ^9.4.0
"@mantine/hooks": ^9.4.0
"@mantine/modals": ^9.4.0
"@mantine/notifications": ^9.4.0
"@mantine/spotlight": ^9.4.0
"@mantine/tiptap": ^9.4.0
"@mantine/charts": ^9.3.2
"@mantine/colors-generator": ^9.3.2
"@mantine/core": ^9.3.2
"@mantine/dates": ^9.3.2
"@mantine/dropzone": ^9.3.2
"@mantine/form": ^9.3.2
"@mantine/hooks": ^9.3.2
"@mantine/modals": ^9.3.2
"@mantine/notifications": ^9.3.2
"@mantine/spotlight": ^9.3.2
"@mantine/tiptap": ^9.3.2
"@next/eslint-plugin-next": 16.2.9
"@octokit/auth-app": ^8.2.0
"@paralleldrive/cuid2": ^3.3.0
"@playwright/test": ^1.61.1
"@playwright/test": ^1.61.0
"@semantic-release/changelog": ^6.0.3
"@semantic-release/commit-analyzer": ^13.0.1
"@semantic-release/git": ^10.0.1
@@ -88,13 +88,13 @@ catalog:
"@types/prismjs": ^1.26.6
"@types/react": 19.2.17
"@types/react-dom": 19.2.3
"@scalar/api-reference-react": ^0.9.48
"@scalar/api-reference-react": ^0.9.47
"@types/video.js": ^7.3.58
"@types/ws": ^8.18.1
"@types/xml2js": ^0.4.14
posthog-js: ^1.393.0
posthog-js: ^1.391.2
posthog-node: ^4.18.0
"@vitejs/plugin-react": ^6.0.3
"@vitejs/plugin-react": ^6.0.2
"@vitest/coverage-v8": ^4.1.9
"@vitest/ui": ^4.1.9
"@xterm/addon-canvas": ^0.7.0
@@ -150,8 +150,8 @@ catalog:
next-intl: 4.13.0
node-loader: ^2.1.0
octokit: ^5.0.5
oxfmt: ^0.56.0
oxlint: ^1.71.0
oxfmt: ^0.55.0
oxlint: ^1.70.0
pg: ^8.22.0
postcss-preset-mantine: ^1.18.0
prismjs: ^1.30.0
@@ -163,7 +163,7 @@ catalog:
react-markdown: ^10.1.0
react-quill-new: ^3.8.3
react-simple-code-editor: ^0.14.1
recharts: ^3.9.0
recharts: ^3.8.1
sass: ^1.101.0
semantic-release: ^25.0.5
semver-parser: ^4.2.1
@@ -177,7 +177,7 @@ catalog:
tsx: 4.20.4
turbo: ^2.9.18
typescript: ^6.0.3
vite: ^8.1.0
vite: ^8.0.16
undici: 7.27.2
use-deep-compare-effect: ^1.8.1
video.js: ^8.23.9
@@ -206,12 +206,12 @@ minimumReleaseAgeExclude:
overrides:
"@babel/helpers@<7.26.10": ">=7.29.7"
"@babel/runtime@<7.26.10": ">=7.29.7"
"axios@>=1.0.0 <1.8.2": ">=1.18.1"
"axios@>=1.0.0 <1.8.2": ">=1.18.0"
"brace-expansion@>=2.0.0 <=2.0.1": ">=5.0.6"
"brace-expansion@>=1.0.0 <=1.1.11": ">=5.0.6"
"esbuild@<=0.24.2": ">=0.28.0"
"form-data@>=4.0.0 <4.0.4": ">=4.0.6"
"hono@<4.6.5": ">=4.12.27"
"hono@<4.6.5": ">=4.12.26"
"linkifyjs@<4.3.2": ">=4.3.3"
"nanoid@>=4.0.0 <5.0.9": ">=5.1.15"
"prismjs@<1.30.0": ">=1.30.0"
@@ -222,7 +222,7 @@ overrides:
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.2"
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.2"
"tmp@<=0.2.3": ">=0.2.7"
"vite@>=5.0.0 <=5.4.18": ">=8.1.0"
"vite@>=5.0.0 <=5.4.18": ">=8.0.16"
patchedDependencies:
"@types/node-unifi": "patches/@types__node-unifi.patch"
"trpc-to-openapi": "patches/trpc-to-openapi.patch"