diff --git a/apps/docs/docs/management/boards/index.mdx b/apps/docs/docs/management/boards/index.mdx
index a9e7bb187..b6cefdac0 100644
--- a/apps/docs/docs/management/boards/index.mdx
+++ b/apps/docs/docs/management/boards/index.mdx
@@ -40,7 +40,7 @@ The column count defines how many horizontal columns are available to drag and d
### Public
This will enable public access for everyone - regardless of whether they are logged in or not.
-This can be really useful if you want to have public boards in your company or for family and friends.
+This can be really useful if you want to have public boards in your company or for family and fiends.
Modifications for public boards must be allowed explicitly using user groups and their permissions.
## Board settings
diff --git a/apps/nextjs/src/components/board/mobile/mobile-board.module.css b/apps/nextjs/src/components/board/mobile/mobile-board.module.css
index 80783dd08..78026b2f1 100644
--- a/apps/nextjs/src/components/board/mobile/mobile-board.module.css
+++ b/apps/nextjs/src/components/board/mobile/mobile-board.module.css
@@ -13,7 +13,7 @@
padding: 0;
margin: -1px;
overflow: hidden;
- clip-path: inset(50%);
+ clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
diff --git a/packages/api/src/router/widgets/beszel.ts b/packages/api/src/router/widgets/beszel.ts
index 16e3e7cb2..738adecf7 100644
--- a/packages/api/src/router/widgets/beszel.ts
+++ b/packages/api/src/router/widgets/beszel.ts
@@ -47,7 +47,6 @@ export const beszelRouter = createTRPCRouter({
};
},
{
- throwOnAllFailure: true,
fallback: (integration, error) => ({
integrationId: integration.id,
integrationName: integration.name,
@@ -120,7 +119,6 @@ export const beszelRouter = createTRPCRouter({
};
},
{
- throwOnAllFailure: true,
fallback: (integration, error) => ({
integrationId: integration.id,
integrationName: integration.name,
diff --git a/packages/api/src/settle-integrations.spec.ts b/packages/api/src/settle-integrations.spec.ts
index ca032b700..52f0d3697 100644
--- a/packages/api/src/settle-integrations.spec.ts
+++ b/packages/api/src/settle-integrations.spec.ts
@@ -23,22 +23,6 @@ describe("settleIntegrationQueries", () => {
expect(results).toEqual(["Second"]);
});
- it("retains fallback and successful results for a partial failure", async () => {
- const results = await settleIntegrationQueries(
- integrations,
- vi.fn(async (integration) => {
- if (integration.id === "first") throw new Error("offline");
- return integration.name;
- }),
- {
- throwOnAllFailure: true,
- fallback: (integration) => `${integration.name} unavailable`,
- },
- );
-
- expect(results).toEqual(["First unavailable", "Second"]);
- });
-
it("throws when every integration fails and throwOnAllFailure is enabled", async () => {
await expect(
settleIntegrationQueries(
@@ -51,21 +35,6 @@ describe("settleIntegrationQueries", () => {
).rejects.toThrow("offline");
});
- it("throws on total failure even when fallbacks are configured", async () => {
- await expect(
- settleIntegrationQueries(
- integrations,
- vi.fn(async () => {
- throw new Error("offline");
- }),
- {
- throwOnAllFailure: true,
- fallback: (integration) => `${integration.name} unavailable`,
- },
- ),
- ).rejects.toThrow("offline");
- });
-
it("does not treat an empty integration list as a failure", async () => {
await expect(
settleIntegrationQueries(
diff --git a/packages/widgets/src/beszel-alerts/component.tsx b/packages/widgets/src/beszel-alerts/component.tsx
index bc3510ac1..708094f8c 100644
--- a/packages/widgets/src/beszel-alerts/component.tsx
+++ b/packages/widgets/src/beszel-alerts/component.tsx
@@ -13,7 +13,6 @@ import { useScopedI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../definition";
import { BeszelIntegrationErrorIndicator } from "../beszel/_shared/error-indicator";
-import { hasStaleIntegrationData } from "../beszel/_shared/query-status";
import { WidgetMobileLoading, WidgetMobileSummary } from "../common/mobile-summary";
const alertIconMap: Record = {
@@ -44,13 +43,7 @@ export default function BeszelAlertsWidget({
() => ({ integrationIds, includeHistory: options.showHistory, maxHistoryItems: options.maxHistoryItems }),
[integrationIds, options.showHistory, options.maxHistoryItems],
);
- const {
- data: results = [],
- error: alertsError,
- isPending,
- isLoadingError: alertsLoadingError,
- isRefetchError: alertsRefetchError,
- } = clientApi.widget.beszel.getAlerts.useQuery(alertsInput);
+ const { data: results = [], error: alertsError, isPending } = clientApi.widget.beszel.getAlerts.useQuery(alertsInput);
const systemNameMap = useMemo(() => {
const map: Record = {};
@@ -79,7 +72,7 @@ export default function BeszelAlertsWidget({
const failedResults = results.filter((result) => "error" in result);
- if (alertsLoadingError) throw alertsError;
+ if (alertsError) throw alertsError;
if (results.length > 0 && failedResults.length === results.length) {
throw new Error(String(failedResults[0]?.error ?? "Unable to connect to Beszel"));
}
@@ -100,7 +93,7 @@ export default function BeszelAlertsWidget({
value={triggeredAlerts.length}
label={t("status.triggered")}
description={`${alerts.length} ${t("name")}`}
- isStale={hasStaleIntegrationData(alertsRefetchError, failedResults)}
+ isStale={failedResults.length > 0}
/>
);
}
@@ -108,7 +101,7 @@ export default function BeszelAlertsWidget({
return (
-
+
diff --git a/packages/widgets/src/beszel-system-grid/component.tsx b/packages/widgets/src/beszel-system-grid/component.tsx
index 5ac7855b8..3b6cc612a 100644
--- a/packages/widgets/src/beszel-system-grid/component.tsx
+++ b/packages/widgets/src/beszel-system-grid/component.tsx
@@ -398,13 +398,11 @@ function BeszelSystemGridContent({
data: results = [],
error: systemsError,
isPending,
- isLoadingError: systemsLoadingError,
- isRefetchError: systemsRefetchError,
} = clientApi.widget.beszel.getSystems.useQuery({ integrationIds });
const filteredSystems = useBeszelFilteredSystems(results, options.statusFilter);
- if (systemsLoadingError) throw systemsError;
+ if (systemsError) throw systemsError;
if (isPending) {
return (
@@ -417,7 +415,7 @@ function BeszelSystemGridContent({
if (filteredSystems.length === 0) {
return (
-
+
@@ -441,7 +439,7 @@ function BeszelSystemGridContent({
return (
-
+
@@ -91,7 +89,7 @@ function BeszelSystemStatsContent({
if (systems.length === 0) {
return (
-
+
@@ -127,7 +125,7 @@ function BeszelSystemStatsContent({
return (
-
+
-
+
"error" in r && r.error);
- if (failed.length === 0 && !isStale) return null;
- const label =
- failed.length > 0
- ? failed.map((r) => r.integrationName ?? r.integrationId).join(", ")
- : t("board.mobile.dataWarning");
+ if (failed.length === 0) return null;
+ const label = failed.map((r) => r.integrationName ?? r.integrationId).join(", ");
return (
-
+
);
}
diff --git a/packages/widgets/src/beszel/_shared/mobile-summary.tsx b/packages/widgets/src/beszel/_shared/mobile-summary.tsx
index d573b50e7..7f1caa6a0 100644
--- a/packages/widgets/src/beszel/_shared/mobile-summary.tsx
+++ b/packages/widgets/src/beszel/_shared/mobile-summary.tsx
@@ -5,7 +5,6 @@ import { useI18n } from "@homarr/translation/client";
import { WidgetMobileLoading, WidgetMobileSummary } from "../../common/mobile-summary";
import { useBeszelFilteredSystems } from "./hooks";
-import { hasStaleIntegrationData } from "./query-status";
interface BeszelSystemsMobileSummaryProps {
integrationIds: string[];
@@ -19,18 +18,12 @@ export const BeszelSystemsMobileSummary = ({
statusFilter,
}: BeszelSystemsMobileSummaryProps) => {
const t = useI18n();
- const {
- data: results = [],
- error,
- isPending,
- isLoadingError,
- isRefetchError,
- } = clientApi.widget.beszel.getSystems.useQuery({ integrationIds });
+ const { data: results = [], error, isPending } = clientApi.widget.beszel.getSystems.useQuery({ integrationIds });
const systems = useBeszelFilteredSystems(results, statusFilter);
const failedResults = results.filter((result) => "error" in result);
if (isPending) return ;
- if (isLoadingError) throw error;
+ if (error) throw error;
if (results.length > 0 && failedResults.length === results.length) {
throw new Error(String(failedResults[0]?.error ?? "Unable to connect to Beszel"));
}
@@ -45,7 +38,7 @@ export const BeszelSystemsMobileSummary = ({
up,
attention: systems.length - up,
})}
- isStale={hasStaleIntegrationData(isRefetchError, failedResults)}
+ isStale={failedResults.length > 0}
/>
);
};
diff --git a/packages/widgets/src/beszel/_shared/query-status.spec.ts b/packages/widgets/src/beszel/_shared/query-status.spec.ts
deleted file mode 100644
index 7663babb7..000000000
--- a/packages/widgets/src/beszel/_shared/query-status.spec.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { describe, expect, test } from "vitest";
-
-import { hasStaleIntegrationData } from "./query-status";
-
-describe("Beszel query status", () => {
- test("marks cached data stale after outer or per-integration failures", () => {
- expect(hasStaleIntegrationData(true, [])).toBe(true);
- expect(hasStaleIntegrationData(false, [{ error: "offline" }])).toBe(true);
- expect(hasStaleIntegrationData(false, [])).toBe(false);
- });
-});
diff --git a/packages/widgets/src/beszel/_shared/query-status.ts b/packages/widgets/src/beszel/_shared/query-status.ts
deleted file mode 100644
index 4d4827486..000000000
--- a/packages/widgets/src/beszel/_shared/query-status.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export const hasStaleIntegrationData = (isRefetchError: boolean, failedResults: readonly unknown[]) =>
- isRefetchError || failedResults.length > 0;
diff --git a/packages/widgets/src/beszel/_shared/stats-view.tsx b/packages/widgets/src/beszel/_shared/stats-view.tsx
index 272b47487..48706d4ad 100644
--- a/packages/widgets/src/beszel/_shared/stats-view.tsx
+++ b/packages/widgets/src/beszel/_shared/stats-view.tsx
@@ -1,7 +1,7 @@
"use client";
import { useMemo } from "react";
-import { Box, Button, Center, Group, SimpleGrid, Skeleton, Stack, Text } from "@mantine/core";
+import { Button, Center, Group, SimpleGrid, Skeleton, Stack, Text } from "@mantine/core";
import { IconPlugConnectedX, IconServerOff } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
@@ -19,7 +19,6 @@ import {
useSystemChartData,
} from "./chart";
import { chartAxisFormatters, formatByteRate, formatGB, formatPercent, formatStorageBytes } from "./format";
-import { BeszelIntegrationErrorIndicator } from "./error-indicator";
import { makeTooltipProps } from "./tooltip";
import { useLiveStats } from "./use-live-stats";
@@ -75,12 +74,7 @@ export function BeszelStatsView({
const showDocker = visibility.dockerCpu || visibility.dockerMemory || visibility.dockerNetwork;
const isLive = timePeriod === "1m";
- const {
- data: historicalData,
- error: historicalError,
- isLoadingError: historicalLoadingError,
- isRefetchError: historicalRefetchError,
- } = clientApi.widget.beszel.getSystemStats.useQuery(
+ const { data: historicalData, error: historicalError } = clientApi.widget.beszel.getSystemStats.useQuery(
{ integrationIds, systemId, timePeriod, includeDocker: showDocker },
{ refetchInterval: isLive ? false : 5_000, enabled: !isLive && systemId !== "" },
);
@@ -181,7 +175,7 @@ export function BeszelStatsView({
timePeriod,
);
- if (!isLive && historicalLoadingError) throw historicalError;
+ if (historicalError) throw historicalError;
if (isLive && liveError) {
return (
@@ -226,10 +220,7 @@ export function BeszelStatsView({
}
return (
-
-
-
-
+
{visibility.cpu && cpuData.length > 0 && (
{
});
});
-describe("storageDeviceNamesMatch", () => {
- test("matches partitions to their base SMART device without merging unrelated devices", () => {
- expect(storageDeviceNamesMatch("/dev/sda1", "/dev/sda")).toBe(true);
- expect(storageDeviceNamesMatch("/dev/nvme0n1p2", "/dev/nvme0n1")).toBe(true);
- expect(storageDeviceNamesMatch("/dev/md0", "/dev/md1")).toBe(false);
- });
-});
-
describe("toScopedStorageVolumeValue", () => {
test("scopes unscoped volume names to the query integration", () => {
expect(toScopedStorageVolumeValue("nas-a", "volume_1")).toBe("nas-a:volume_1");
diff --git a/packages/widgets/src/filter-storage-volumes.ts b/packages/widgets/src/filter-storage-volumes.ts
index f5d294508..5e88584be 100644
--- a/packages/widgets/src/filter-storage-volumes.ts
+++ b/packages/widgets/src/filter-storage-volumes.ts
@@ -28,14 +28,18 @@ export const toScopedStorageVolumeValue = (integrationId: string, value: string)
return `${integrationId}:${volumeName}`;
};
-export const storageDeviceNamesMatch = (leftDeviceName: string, rightDeviceName: string): boolean => {
+const storageDeviceNamesMatch = (leftDeviceName: string, rightDeviceName: string): boolean => {
return (
leftDeviceName === rightDeviceName ||
normalizeStorageDeviceName(leftDeviceName) === normalizeStorageDeviceName(rightDeviceName)
);
};
-const matchesVisibleStorageVolume = (visibleVolume: string, integrationId: string, deviceName: string): boolean => {
+const matchesVisibleStorageVolume = (
+ visibleVolume: string,
+ integrationId: string,
+ deviceName: string,
+): boolean => {
const separatorIndex = visibleVolume.indexOf(":");
if (separatorIndex === -1) {
return storageDeviceNamesMatch(visibleVolume, deviceName);
diff --git a/packages/widgets/src/system-disks/component.tsx b/packages/widgets/src/system-disks/component.tsx
index 19a09ccf7..8db1129aa 100644
--- a/packages/widgets/src/system-disks/component.tsx
+++ b/packages/widgets/src/system-disks/component.tsx
@@ -10,10 +10,9 @@ import { useI18n } from "@homarr/translation/client";
import { WidgetEmptyState } from "../common/empty-state";
import { WidgetMobileLoading, WidgetMobileSummary } from "../common/mobile-summary";
-import { NoIntegrationDataError } from "../errors/no-data-integration";
-import { filterStorageVolumes, storageDeviceNamesMatch } from "../filter-storage-volumes";
import type { WidgetComponentProps } from "../definition";
-import { formatDiskTemperature } from "./mobile-summary";
+import { filterStorageVolumes } from "../filter-storage-volumes";
+import { NoIntegrationDataError } from "../errors/no-data-integration";
type DisplayMode = WidgetComponentProps<"systemDisks">["options"]["displayMode"];
@@ -82,7 +81,6 @@ const SystemDiskCard = ({
}, [displayText, healthy]);
const unhealthyLabel = t("widget.systemDisks.status.unhealthy");
- const temperatureLabel = formatDiskTemperature(temperature, true);
return (
{unhealthyLabel}}
- {temperatureLabel ?
{temperatureLabel}
: null}
+ {temperature ?
{temperature}°C
: null}
diskB.percentage - diskA.percentage)[0];
if (!disk) return ;
- const smartItem = smart.find((item) => storageDeviceNamesMatch(item.deviceName, disk.deviceName));
+ const smartItem = smart.find((item) => item.deviceName === disk.deviceName);
const isHealthy = smartItem?.healthy ?? true;
- const temperature = formatDiskTemperature(smartItem?.temperature, options.showTemperatureIfAvailable);
+ const temperature =
+ options.showTemperatureIfAvailable && smartItem?.temperature ? `${smartItem.temperature}°C` : undefined;
return (
{fileSystem.map((item) => {
- const smartItem = smart.find((smartEntry) => storageDeviceNamesMatch(smartEntry.deviceName, item.deviceName));
+ const smartItem = smart.find((smartEntry) => smartEntry.deviceName === item.deviceName);
return (
{
- test("preserves a valid zero-degree reading", () => {
- expect(formatDiskTemperature(0, true)).toBe("0°C");
- });
-
- test("omits unavailable or disabled readings", () => {
- expect(formatDiskTemperature(null, true)).toBeUndefined();
- expect(formatDiskTemperature(undefined, true)).toBeUndefined();
- expect(formatDiskTemperature(20, false)).toBeUndefined();
- });
-});
diff --git a/packages/widgets/src/system-disks/mobile-summary.ts b/packages/widgets/src/system-disks/mobile-summary.ts
deleted file mode 100644
index 123d0dd7d..000000000
--- a/packages/widgets/src/system-disks/mobile-summary.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export const formatDiskTemperature = (temperature: number | null | undefined, shouldShow: boolean) =>
- shouldShow && temperature != null ? `${temperature}°C` : undefined;
diff --git a/packages/widgets/src/weather/component.tsx b/packages/widgets/src/weather/component.tsx
index b25a70a25..366fc6827 100644
--- a/packages/widgets/src/weather/component.tsx
+++ b/packages/widgets/src/weather/component.tsx
@@ -14,7 +14,6 @@ import { WidgetEmptyState } from "../common/empty-state";
import type { WidgetComponentProps } from "../definition";
import { AnimatedWeatherIcon } from "./animated-icon";
import { WeatherDescription } from "./icon";
-import { getPreferredUnit } from "./temperature";
export default function WeatherWidget({ isEditMode, options }: WidgetComponentProps<"weather">) {
const input = {
@@ -248,3 +247,10 @@ function Forecast({ weather, options }: WeatherProps) {
);
}
+
+const getPreferredUnit = (value?: number, isFahrenheit = false, disableTemperatureDecimals = false): string =>
+ value
+ ? isFahrenheit
+ ? `${(value * (9 / 5) + 32).toFixed(disableTemperatureDecimals ? 0 : 1)}°F`
+ : `${value.toFixed(disableTemperatureDecimals ? 0 : 1)}°C`
+ : "?";
diff --git a/packages/widgets/src/weather/temperature.spec.ts b/packages/widgets/src/weather/temperature.spec.ts
deleted file mode 100644
index d22b96619..000000000
--- a/packages/widgets/src/weather/temperature.spec.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { describe, expect, test } from "vitest";
-
-import { getPreferredUnit } from "./temperature";
-
-describe("getPreferredUnit", () => {
- test("preserves zero in Celsius and Fahrenheit", () => {
- expect(getPreferredUnit(0)).toBe("0.0°C");
- expect(getPreferredUnit(0, true)).toBe("32.0°F");
- });
-
- test("only uses the unavailable placeholder for missing values", () => {
- expect(getPreferredUnit(undefined)).toBe("?");
- });
-
- test("respects the decimal preference", () => {
- expect(getPreferredUnit(0, false, true)).toBe("0°C");
- expect(getPreferredUnit(0, true, true)).toBe("32°F");
- expect(getPreferredUnit(12.6, false, true)).toBe("13°C");
- });
-});
diff --git a/packages/widgets/src/weather/temperature.ts b/packages/widgets/src/weather/temperature.ts
deleted file mode 100644
index 73edf7f87..000000000
--- a/packages/widgets/src/weather/temperature.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export const getPreferredUnit = (value?: number, isFahrenheit = false, disableTemperatureDecimals = false): string =>
- value !== undefined
- ? isFahrenheit
- ? `${(value * (9 / 5) + 32).toFixed(disableTemperatureDecimals ? 0 : 1)}°F`
- : `${value.toFixed(disableTemperatureDecimals ? 0 : 1)}°C`
- : "?";