diff --git a/apps/nextjs/src/components/board/items/item-content.tsx b/apps/nextjs/src/components/board/items/item-content.tsx
index aece414cf..c40b8a57a 100644
--- a/apps/nextjs/src/components/board/items/item-content.tsx
+++ b/apps/nextjs/src/components/board/items/item-content.tsx
@@ -5,6 +5,7 @@ import { useElementSize } from "@mantine/hooks";
import { QueryErrorResetBoundary, useQueryClient } from "@tanstack/react-query";
import combineClasses from "clsx";
import { NoIntegrationSelectedError } from "@homarr/widgets/errors/classes";
+import type { FallbackProps } from "react-error-boundary";
import { ErrorBoundary } from "react-error-boundary";
import { useRequiredBoard } from "@homarr/boards/context";
@@ -33,6 +34,12 @@ const getOverflowFromKind = (kind: SectionItem["kind"]) => {
return undefined;
};
+const WidgetDefinitionLoadError = ({ error, resetErrorBoundary }: FallbackProps) => (
+
+
+
+);
+
export const BoardItemContent = ({ item }: BoardItemContentProps) => {
const { ref, width, height } = useElementSize();
const board = useRequiredBoard();
@@ -40,21 +47,23 @@ export const BoardItemContent = ({ item }: BoardItemContentProps) => {
return (
<>
-
-
-
- }
- >
-
-
+
+
+
+
+ }
+ >
+
+
+
{item.advancedOptions.title?.trim() && (
buildSortedIntegrations({ enableMockIntegration, onboarding }),
});
@@ -136,7 +140,14 @@ export const IntegrationMultiSelectGrid = ({
);
})}
- {!isPending && filtered.length === 0 && (
+ {isError && (
+
+
+ {t("common.error")}
+
+
+ )}
+ {!isPending && !isError && 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 017f0acf5..e6458df6e 100644
--- a/apps/nextjs/src/components/integration/integration-select-grid.tsx
+++ b/apps/nextjs/src/components/integration/integration-select-grid.tsx
@@ -20,7 +20,11 @@ export const IntegrationSelectGrid = ({ onSelect, enableMockIntegration = false
const [search, setSearch] = useState("");
const t = useI18n();
- const { data: integrations = [], isPending } = useQuery({
+ const {
+ data: integrations = [],
+ isPending,
+ isError,
+ } = useQuery({
queryKey: ["widget-integrations", { enableMockIntegration }],
queryFn: () => buildSortedIntegrations({ enableMockIntegration }),
});
@@ -89,7 +93,14 @@ export const IntegrationSelectGrid = ({ onSelect, enableMockIntegration = false
))}
- {!isPending && filtered.length === 0 && (
+ {isError && (
+
+
+ {t("common.error")}
+
+
+ )}
+ {!isPending && !isError && filtered.length === 0 && (
{t("common.noResults")}
diff --git a/apps/nextjs/src/instrumentation.spec.ts b/apps/nextjs/src/instrumentation.spec.ts
index 384e3ddad..c8cb5d586 100644
--- a/apps/nextjs/src/instrumentation.spec.ts
+++ b/apps/nextjs/src/instrumentation.spec.ts
@@ -52,4 +52,22 @@ describe("Next instrumentation", () => {
expect(tasksStarted).not.toHaveBeenCalled();
expect(websocketStarted).not.toHaveBeenCalled();
});
+
+ test("isolates embedded service startup failures", async () => {
+ const websocketStarted = vi.fn();
+ vi.stubEnv("NODE_ENV", "production");
+ vi.stubEnv("NEXT_RUNTIME", "nodejs");
+ vi.doMock("@homarr/tasks", () => {
+ throw new Error("task startup failed");
+ });
+ vi.doMock("@homarr/websocket", () => {
+ websocketStarted();
+ return {};
+ });
+
+ const { register } = await import("./instrumentation");
+
+ await expect(register()).resolves.toBeUndefined();
+ expect(websocketStarted).toHaveBeenCalledOnce();
+ });
});
diff --git a/apps/nextjs/src/instrumentation.ts b/apps/nextjs/src/instrumentation.ts
index d6d55f3fc..d73053bec 100644
--- a/apps/nextjs/src/instrumentation.ts
+++ b/apps/nextjs/src/instrumentation.ts
@@ -1,7 +1,22 @@
+import { createLogger } from "@homarr/core/infrastructure/logs";
+
+const logger = createLogger({ module: "instrumentation" });
+
export async function register() {
if (process.env.NEXT_RUNTIME !== "nodejs") return;
if (process.env.NODE_ENV !== "production") return;
- await import("@homarr/tasks");
- await import("@homarr/websocket");
+ const services = [
+ { name: "tasks", start: () => import("@homarr/tasks") },
+ { name: "websocket", start: () => import("@homarr/websocket") },
+ ];
+ const results = await Promise.allSettled(services.map(({ start }) => start()));
+
+ for (const [index, result] of results.entries()) {
+ if (result.status === "rejected") {
+ logger.error(
+ new Error(`Failed to start embedded ${services[index]?.name ?? "unknown"} service`, { cause: result.reason }),
+ );
+ }
+ }
}