diff --git a/apps/nextjs/src/app/[locale]/widgets/[kind]/page.tsx b/apps/nextjs/src/app/[locale]/widgets/[kind]/page.tsx
index 56ff3e215..00d54fc1a 100644
--- a/apps/nextjs/src/app/[locale]/widgets/[kind]/page.tsx
+++ b/apps/nextjs/src/app/[locale]/widgets/[kind]/page.tsx
@@ -1,7 +1,7 @@
+import { Suspense } from "react";
import { notFound } from "next/navigation";
-import { Center } from "@mantine/core";
+import { Center, Loader } from "@mantine/core";
-import { env } from "@homarr/common/env";
import { db } from "@homarr/db";
import type { WidgetKind } from "@homarr/definitions";
import { widgetKinds } from "@homarr/widgets/manifest";
@@ -13,7 +13,8 @@ interface Props {
}
export default async function WidgetPreview(props: Props) {
- if (!(widgetKinds.includes((await props.params).kind as WidgetKind) || env.NODE_ENV !== "development")) {
+ const { kind } = await props.params;
+ if (!widgetKinds.includes(kind as WidgetKind)) {
notFound();
}
@@ -26,11 +27,11 @@ export default async function WidgetPreview(props: Props) {
},
});
- const sort = (await props.params).kind as WidgetKind;
-
return (
-
+ }>
+
+
);
}
diff --git a/apps/nextjs/src/components/integration/integration-multi-select-grid.tsx b/apps/nextjs/src/components/integration/integration-multi-select-grid.tsx
index ff1bd4ff1..44e947557 100644
--- a/apps/nextjs/src/components/integration/integration-multi-select-grid.tsx
+++ b/apps/nextjs/src/components/integration/integration-multi-select-grid.tsx
@@ -136,7 +136,7 @@ export const IntegrationMultiSelectGrid = ({
);
})}
- {filtered.length === 0 && (
+ {!isPending && filtered.length === 0 && (
{t("common.noResults")}
diff --git a/apps/nextjs/src/components/integration/integration-select-grid.tsx b/apps/nextjs/src/components/integration/integration-select-grid.tsx
index 5127534d9..017f0acf5 100644
--- a/apps/nextjs/src/components/integration/integration-select-grid.tsx
+++ b/apps/nextjs/src/components/integration/integration-select-grid.tsx
@@ -89,7 +89,7 @@ export const IntegrationSelectGrid = ({ onSelect, enableMockIntegration = false
))}
- {filtered.length === 0 && (
+ {!isPending && filtered.length === 0 && (
{t("common.noResults")}
diff --git a/packages/db/proxy-reader.spec.ts b/packages/db/proxy-reader.spec.ts
index 16fa37d9a..f46c79cef 100644
--- a/packages/db/proxy-reader.spec.ts
+++ b/packages/db/proxy-reader.spec.ts
@@ -44,6 +44,7 @@ describe("proxy database pools", () => {
vi.resetModules();
vi.clearAllMocks();
for (const key of Object.keys(mocks.dbEnv)) delete mocks.dbEnv[key];
+ mocks.mysqlCreatePool.mockReturnValue({});
});
it("bounds MySQL URL pools instead of falling back to the driver defaults", async () => {
@@ -108,4 +109,20 @@ describe("proxy database pools", () => {
expect(mocks.postgresPool).toHaveBeenCalledWith(expect.objectContaining({ max: 1 }));
});
+
+ it("retries initialization after a transient driver failure", async () => {
+ Object.assign(mocks.dbEnv, {
+ DRIVER: "mysql2",
+ URL: "mysql://homarr:test@localhost:3306/homarr",
+ });
+ mocks.mysqlCreatePool.mockImplementationOnce(() => {
+ throw new Error("database unavailable");
+ });
+
+ const { getOnboardingStepForProxyAsync } = await import("./proxy-reader");
+
+ await expect(getOnboardingStepForProxyAsync()).rejects.toThrow("database unavailable");
+ await expect(getOnboardingStepForProxyAsync()).resolves.toBe("finish");
+ expect(mocks.mysqlCreatePool).toHaveBeenCalledTimes(2);
+ });
});
diff --git a/packages/db/proxy-reader.ts b/packages/db/proxy-reader.ts
index d0bb98319..1fd85f223 100644
--- a/packages/db/proxy-reader.ts
+++ b/packages/db/proxy-reader.ts
@@ -152,7 +152,12 @@ const createProxyReaderAsync = async (): Promise => {
let proxyReaderPromise: Promise | null = null;
const getProxyReaderAsync = () => {
- proxyReaderPromise ??= createProxyReaderAsync();
+ if (!proxyReaderPromise) {
+ proxyReaderPromise = createProxyReaderAsync().catch((error: unknown) => {
+ proxyReaderPromise = null;
+ throw error;
+ });
+ }
return proxyReaderPromise;
};
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 15439d57b..49d8f0dc8 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -33,7 +33,6 @@ catalog:
"@mantine/notifications": ^9.4.1
"@mantine/spotlight": ^9.4.1
"@mantine/tiptap": ^9.4.1
- "@next/eslint-plugin-next": 16.2.10
"@octokit/auth-app": ^8.2.0
"@paralleldrive/cuid2": ^3.3.0
"@playwright/test": ^1.61.1
diff --git a/scripts/benchmarks/dev-benchmark-lib.mts b/scripts/benchmarks/dev-benchmark-lib.mts
index ee20502a3..858e8fe7d 100644
--- a/scripts/benchmarks/dev-benchmark-lib.mts
+++ b/scripts/benchmarks/dev-benchmark-lib.mts
@@ -30,7 +30,11 @@ export const summarize = (numbers: Array): NumericSum
return { count: 0, medianMs: null, p95Ms: null, standardDeviationMs: null, outliersMs: [] };
}
- const medianIndex = Math.floor(sorted.length / 2);
+ const middleIndex = Math.floor(sorted.length / 2);
+ let medianMs = sorted[middleIndex] ?? 0;
+ if (sorted.length % 2 === 0) {
+ medianMs = ((sorted[middleIndex - 1] ?? 0) + medianMs) / 2;
+ }
const p95Index = Math.min(sorted.length - 1, Math.ceil(sorted.length * 0.95) - 1);
const mean = sorted.reduce((total, value) => total + value, 0) / sorted.length;
const variance = sorted.reduce((total, value) => total + (value - mean) ** 2, 0) / sorted.length;
@@ -41,7 +45,7 @@ export const summarize = (numbers: Array): NumericSum
return {
count: sorted.length,
- medianMs: sorted[medianIndex] ?? 0,
+ medianMs,
p95Ms: sorted[p95Index] ?? 0,
standardDeviationMs: Math.round(Math.sqrt(variance)),
outliersMs: sorted.filter((value) => value > outlierLimit),
diff --git a/scripts/benchmarks/dev-benchmark-lib.spec.ts b/scripts/benchmarks/dev-benchmark-lib.spec.ts
index f1e113376..fa374270c 100644
--- a/scripts/benchmarks/dev-benchmark-lib.spec.ts
+++ b/scripts/benchmarks/dev-benchmark-lib.spec.ts
@@ -26,6 +26,10 @@ describe("development benchmark measurements", () => {
});
});
+ it("averages both middle values for an even sample count", () => {
+ expect(summarize([100, 200]).medianMs).toBe(150);
+ });
+
it("requires a successful authenticated board hydration and interaction", () => {
expect(
validateBrowserMeasurement({