diff --git a/apps/docs/docs/widgets/beszel-alerts/index.mdx b/apps/docs/docs/widgets/beszel-alerts/index.mdx index 25dbf12ab..160e3a732 100644 --- a/apps/docs/docs/widgets/beszel-alerts/index.mdx +++ b/apps/docs/docs/widgets/beszel-alerts/index.mdx @@ -17,7 +17,7 @@ import beszelAlerts from "./img/beszel-alerts.png"; -This widget displays your Beszel alert configurations separated by status (triggered and ok). It also shows alert history with timestamps. Compact mode keeps configured OK alerts, descriptions, and history visible alongside triggered alerts. Alerts are updated in real time via WebSocket subscriptions. +This widget displays your Beszel alert configurations separated by status (triggered and ok). It also shows alert history with timestamps. Compact mode keeps configured OK alerts and descriptions visible alongside triggered alerts, and keeps history visible when the history option is enabled. Alerts are updated in real time via WebSocket subscriptions. ### Screenshots diff --git a/apps/docs/docs/widgets/minecraft-server-status/index.mdx b/apps/docs/docs/widgets/minecraft-server-status/index.mdx index c35135292..e66344c85 100644 --- a/apps/docs/docs/widgets/minecraft-server-status/index.mdx +++ b/apps/docs/docs/widgets/minecraft-server-status/index.mdx @@ -17,7 +17,7 @@ import { minecraftServerStatusWidget } from "."; -Shows the status and amount of players on a Minecraft server. Compact mode keeps the server icon and player count visible instead of hiding them based on tile size. It supports both Java and Bedrock editions, however only public servers are supported which can be reached through the used api of [api.mcsrvstat.us](https://api.mcsrvstat.us). +Shows the status and amount of players on a Minecraft server. Compact mode keeps the server icon and player count visible instead of hiding them based on tile size. It supports both Java and Bedrock editions, but only public servers accessible through the [mcsrvstat.us API](https://api.mcsrvstat.us) are supported. ### Screenshots diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 59e09e437..aed89193d 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -3179,7 +3179,8 @@ "disabled": "Disabled" }, "error": { - "internalServerError": "Failed to fetch indexers status" + "internalServerError": "Failed to fetch indexers status", + "testAll": "The indexer test failed. Try again." } }, "healthMonitoring": { @@ -4249,6 +4250,7 @@ "failed": "Failed", "completed": "Completed" }, + "unknown": "Unknown", "toBeDetermined": "TBD" }, "mediaRequests-requestStats": { @@ -4961,6 +4963,10 @@ "empty": "No alerts configured", "history": "History", "resolved": "Resolved", + "description": { + "statusDuration": "Down for {minutes} min", + "threshold": "Exceeds {value} over {minutes} min" + }, "status": { "triggered": "Triggered", "ok": "OK" diff --git a/packages/widgets/src/audio-stats/audio-stats-shared.spec.ts b/packages/widgets/src/audio-stats/audio-stats-shared.spec.ts index 77534fec7..1d6d4b2a7 100644 --- a/packages/widgets/src/audio-stats/audio-stats-shared.spec.ts +++ b/packages/widgets/src/audio-stats/audio-stats-shared.spec.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from "vitest"; import type { AudiobookshelfDashboardData, NavidromeDashboardData } from "@homarr/integrations/types"; -import { getGridCols, getIconSize, getVisibleStatLimit, getVisibleStats, prioritizeVisibleStats } from "./shared"; +import { getGridCols, getIconSize, getVisibleStats } from "./shared"; describe("getGridCols", () => { test("caps columns to visible stat count", () => { @@ -37,17 +37,6 @@ describe("getIconSize", () => { }); }); -describe("getVisibleStatLimit", () => { - test("keeps compact metrics legible when height is constrained", () => { - expect(getVisibleStatLimit(100, 80, 5, true)).toBe(1); - expect(getVisibleStatLimit(180, 50, 5, true)).toBe(3); - }); - - test("preserves every configured metric outside compact mode", () => { - expect(getVisibleStatLimit(100, 80, 5, false)).toBe(5); - }); -}); - describe("getVisibleStats", () => { const navidromeStats: NavidromeDashboardData = { artistCount: 100, @@ -117,21 +106,3 @@ describe("getVisibleStats", () => { expect(typeof result[0]?.value).toBe("string"); }); }); - -describe("prioritizeVisibleStats", () => { - test("puts an active session first only on compact surfaces", () => { - const stats = getVisibleStats( - "audiobookshelf", - { showLibraryCount: true, showActiveSessions: true }, - { - libraryCount: 3, - totalAudiobooks: 150, - totalPodcasts: 20, - totalListeningTimeSeconds: 86400, - activeSessions: 2, - }, - ); - expect(prioritizeVisibleStats(stats, true)[0]?.statKey).toBe("activeSessions"); - expect(prioritizeVisibleStats(stats, false)[0]?.statKey).toBe("libraryCount"); - }); -}); diff --git a/packages/widgets/src/audio-stats/shared.ts b/packages/widgets/src/audio-stats/shared.ts index 75a875b6a..f31f55592 100644 --- a/packages/widgets/src/audio-stats/shared.ts +++ b/packages/widgets/src/audio-stats/shared.ts @@ -156,23 +156,6 @@ export const getGridCols = (width: number, visibleCount: number, compact: boolea return Math.min(maxCols, Math.max(visibleCount, 1)); }; -export const getVisibleStatLimit = (width: number, height: number, visibleCount: number, compact: boolean): number => { - if (!compact) return visibleCount; - - const columns = getGridCols(width, visibleCount, true); - const rows = Math.max(1, Math.floor(Math.max(0, height - 8) / 44)); - return Math.min(visibleCount, columns * rows); -}; - -export const prioritizeVisibleStats = (stats: VisibleStat[], compact: boolean): VisibleStat[] => { - if (!compact) return stats; - return stats.toSorted( - (left, right) => - Number(right.statKey === "activeSessions" && Number(right.value) > 0) - - Number(left.statKey === "activeSessions" && Number(left.value) > 0), - ); -}; - const fallbackIconSize = { true: 14, false: 16 } as const; export const getIconSize = (width: number, compact: boolean): number => { diff --git a/packages/widgets/src/beszel-alerts/component.tsx b/packages/widgets/src/beszel-alerts/component.tsx index 17beda443..9069cc8f8 100644 --- a/packages/widgets/src/beszel-alerts/component.tsx +++ b/packages/widgets/src/beszel-alerts/component.tsx @@ -252,15 +252,14 @@ const unitSuffixMap: Record = { Status: "", }; -function formatAlertDescription(name: string, value: number, min: number): string { - const suffix = unitSuffixMap[name] ?? ""; - if (name === "Status") return `down for ${min} min`; - return `exceeds ${value}${suffix} over ${min} min`; -} - function AlertRow({ name, value, min, systemName, integrationName, triggered, isAdvanced }: AlertRowProps) { + const t = useI18n("widget.beszelAlerts"); const Icon = alertIconMap[name] ?? Server; - const description = formatAlertDescription(name, value, min); + const suffix = unitSuffixMap[name] ?? ""; + let description = t("description.threshold", { value: `${value}${suffix}`, minutes: min }); + if (name === "Status") { + description = t("description.statusDuration", { minutes: min }); + } const accentColor = triggered ? "red" : "green"; const borderColor = triggered ? "red" : "gray"; const rowStyle = isAdvanced diff --git a/packages/widgets/src/immich/server-stats/component.module.css b/packages/widgets/src/immich/server-stats/component.module.css index 85fa52d86..19436a676 100644 --- a/packages/widgets/src/immich/server-stats/component.module.css +++ b/packages/widgets/src/immich/server-stats/component.module.css @@ -29,6 +29,6 @@ .statItem, .statItemDense, .statItemCompact { - border-color: CanvasText; + border: 0.0625rem solid CanvasText; } } diff --git a/packages/widgets/src/indexer-manager/component.tsx b/packages/widgets/src/indexer-manager/component.tsx index 209c639b7..c0d809b97 100644 --- a/packages/widgets/src/indexer-manager/component.tsx +++ b/packages/widgets/src/indexer-manager/component.tsx @@ -23,6 +23,7 @@ import combineClasses from "clsx"; import { clientApi } from "@homarr/api/client"; import { useIntegrationsWithInteractAccess } from "@homarr/auth/client"; import { useRequiredBoard } from "@homarr/boards/context"; +import { showErrorNotification } from "@homarr/notifications"; import { useI18n } from "@homarr/translation/client"; import { iconSizes } from "@homarr/ui"; @@ -62,6 +63,11 @@ export default function IndexerManagerWidget({ isPending, error: testAllError, } = clientApi.widget.indexerManager.testAllIndexers.useMutation({ + onError: () => + showErrorNotification({ + title: tCommon("error"), + message: t("error.testAll"), + }), onSettled: () => void utils.widget.indexerManager.getIndexersStatus.invalidate(), }); const interactIntegrationIds = new Set(useIntegrationsWithInteractAccess().map(({ id }) => id)); diff --git a/packages/widgets/src/media-requests/list/component.tsx b/packages/widgets/src/media-requests/list/component.tsx index a6d46763b..617b332be 100644 --- a/packages/widgets/src/media-requests/list/component.tsx +++ b/packages/widgets/src/media-requests/list/component.tsx @@ -189,7 +189,7 @@ const MediaRequestCard = ({ request, isTiny, showIntegrationSource, canInteract, lineClamp={1} style={{ wordBreak: "break-all" }} > - {(request.requestedBy?.displayName ?? "") || "unknown"} + {(request.requestedBy?.displayName ?? "") || t("unknown")} @@ -207,7 +207,7 @@ const MediaRequestCard = ({ request, isTiny, showIntegrationSource, canInteract, lineClamp={1} style={{ minWidth: 0 }} > - {request.name || "unknown"} + {request.name || t("unknown")} {request.status === "pending" ? ( { expect( getNetworkControllerStatusLayout({ width: 199, height: 119, displayMode: "compact", content: "wifi" }), ).toMatchObject({ padding: 4, horizontalStats: true }); + expect( + getNetworkControllerStatusLayout({ width: 199, height: 80, displayMode: "compact", content: "wifi" }), + ).toMatchObject({ inlineStats: false }); + expect( + getNetworkControllerStatusLayout({ width: 199, height: 79, displayMode: "compact", content: "wifi" }), + ).toMatchObject({ inlineStats: true }); expect( getNetworkControllerStatusLayout({ width: 960, height: 320, displayMode: "advanced", content: "wifi" }), ).toMatchObject({ sourceColumns: 2 }); diff --git a/packages/widgets/src/network-controller/summary/component.tsx b/packages/widgets/src/network-controller/summary/component.tsx index ad3c85d36..873f28077 100644 --- a/packages/widgets/src/network-controller/summary/component.tsx +++ b/packages/widgets/src/network-controller/summary/component.tsx @@ -63,9 +63,23 @@ export default function NetworkControllerSummaryWidget({ ); + const firstSummary = summaries[0]; + + if (isPending || !firstSummary) { + return ( + + {queryIndicators} +
+ + {isPending ? tCommon("action.loading") : tWidgetCommon("integrationDisconnected")} + +
+
+ ); + } if (!isAdvanced) { - const summary = summaries[0]?.summary; + const summary = firstSummary.summary; return ( @@ -77,8 +91,8 @@ export default function NetworkControllerSummaryWidget({ } @@ -88,8 +102,8 @@ export default function NetworkControllerSummaryWidget({ } @@ -97,15 +111,15 @@ export default function NetworkControllerSummaryWidget({ WWW - {summary?.www.latency ?? 0}ms + {summary.www.latency}ms } @@ -115,8 +129,8 @@ export default function NetworkControllerSummaryWidget({ } @@ -124,7 +138,7 @@ export default function NetworkControllerSummaryWidget({ {t("card.vpn.label")} - {t("card.vpn.countConnected", { count: summary?.vpn.users ?? 0 })} + {t("card.vpn.countConnected", { count: summary.vpn.users })} @@ -134,19 +148,6 @@ export default function NetworkControllerSummaryWidget({ ); } - if (isPending || summaries.length === 0) { - return ( - - {queryIndicators} -
- - {isPending ? tCommon("action.loading") : tWidgetCommon("integrationDisconnected")} - -
-
- ); - } - const controllerColumns = width >= 960 ? 2 : 1; const matrixColumns = width >= 720 ? 3 : width >= 440 ? 2 : 1; diff --git a/packages/widgets/src/system-disks/component.tsx b/packages/widgets/src/system-disks/component.tsx index 557a8cb85..2fe0c9829 100644 --- a/packages/widgets/src/system-disks/component.tsx +++ b/packages/widgets/src/system-disks/component.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useMemo, useRef, useState } from "react"; -import { Box, Card, Group, ScrollArea, SimpleGrid, Text, Tooltip, useMantineColorScheme } from "@mantine/core"; +import { Box, Card, Group, ScrollArea, SimpleGrid, Text, Tooltip, useComputedColorScheme } from "@mantine/core"; import { clientApi } from "@homarr/api/client"; import { useRequiredBoard } from "@homarr/boards/context"; @@ -113,7 +113,7 @@ const SystemDiskCard = ({ showTemperature, }: SystemDiskCardProps) => { const board = useRequiredBoard(); - const scheme = useMantineColorScheme(); + const colorScheme = useComputedColorScheme("light"); const t = useI18n("widget.systemDisks"); const valueRef = useRef(null); const [valueFits, setValueFits] = useState(true); @@ -150,7 +150,7 @@ const SystemDiskCard = ({ .join(" ยท "); const backgroundColor = "rgb(from var(--mantine-color-primaryColor-filled) r g b / calc(var(--opacity, 1) * 0.12))"; const borderColor = "rgb(from var(--mantine-color-secondaryColor-filled) r g b / calc(var(--opacity, 1) * 0.45))"; - const legacyBackground = scheme.colorScheme === "dark" ? "dark.7" : "gray.1"; + const legacyBackground = colorScheme === "dark" ? "dark.7" : "gray.1"; const cardBackground = isAdvanced ? backgroundColor : legacyBackground; const progressBackground = healthy ? "var(--mantine-color-green-light)" : "var(--mantine-color-red-light)"; const legacyProgressColor = healthy ? "green" : "red"; diff --git a/packages/widgets/src/timetable/component.tsx b/packages/widgets/src/timetable/component.tsx index 34759ad6a..16fd607b6 100644 --- a/packages/widgets/src/timetable/component.tsx +++ b/packages/widgets/src/timetable/component.tsx @@ -167,7 +167,13 @@ function DepartureRow({ entry, dense, locale }: { entry: TimetableEntryView; den {entry.platform && ( - + {entry.platform.name} )}