diff --git a/packages/widgets/src/audio-stats/component.tsx b/packages/widgets/src/audio-stats/component.tsx
index 5863b8ed4..12a4f25e7 100644
--- a/packages/widgets/src/audio-stats/component.tsx
+++ b/packages/widgets/src/audio-stats/component.tsx
@@ -1,15 +1,18 @@
"use client";
-import { Badge, Group, ScrollArea, SimpleGrid, Stack, Text } from "@mantine/core";
+import { Badge, Box, Group, ScrollArea, SimpleGrid, Stack, Text } from "@mantine/core";
+import { getQueryKey } from "@trpc/react-query";
import { clientApi } from "@homarr/api/client";
import { formatBytes, formatDuration } from "@homarr/common";
import type { AudiobookshelfDashboardData } from "@homarr/integrations/types";
+import { useI18n } from "@homarr/translation/client";
import { WidgetEmptyState } from "../common/empty-state";
+import { IntegrationErrorIndicator } from "../common/integration-error-indicator";
+import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
import type { WidgetComponentProps } from "../definition";
-import { setWidgetRuntimeQueries } from "../definition";
-import { NoIntegrationDataError } from "../errors/no-data-integration";
+import { useWidgetRuntimeQueries } from "../runtime-hooks";
import { AudioStatsContent } from "./audio-stats-content";
export default function AudioStatsWidget({
@@ -18,8 +21,9 @@ export default function AudioStatsWidget({
width,
height,
displayMode = "compact",
- widgetStateRef,
+ widgetRuntimeRef,
}: WidgetComponentProps<"audioStats">) {
+ const t = useI18n();
const statsInput = { integrationId: integrationIds[0] ?? "" };
const { data: response, error: statsError } = clientApi.widget.audioStats.getStats.useQuery(statsInput);
const streamsInput = { integrationIds: [statsInput.integrationId], showOnlyPlaying: true };
@@ -28,15 +32,14 @@ export default function AudioStatsWidget({
streamsInput,
{ enabled: streamsEnabled },
);
- setWidgetRuntimeQueries(widgetStateRef, [
- { path: ["widget", "audioStats", "getStats"], input: statsInput },
- ...(streamsEnabled ? [{ path: ["widget", "mediaServer", "getCurrentStreams"], input: streamsInput }] : []),
+ useWidgetRuntimeQueries(widgetRuntimeRef, [
+ getQueryKey(clientApi.widget.audioStats.getStats, statsInput, "query"),
+ ...(streamsEnabled ? [getQueryKey(clientApi.widget.mediaServer.getCurrentStreams, streamsInput, "query")] : []),
]);
if (statsError && response === undefined) throw statsError;
if (streamsEnabled && streamsError && streamResults === undefined) throw streamsError;
const currentStreams = streamResults ?? [];
- if (streamsEnabled && currentStreams.some(({ error }) => Boolean(error))) throw new NoIntegrationDataError();
if (!response) return ;
const summary = (
@@ -44,20 +47,38 @@ export default function AudioStatsWidget({
backend={response.kind}
stats={response.data}
options={{ ...options, compactMode: displayMode === "compact" && (options.compactMode || height < 160) }}
+ showAllStats={displayMode === "advanced"}
width={width}
height={height}
/>
);
- if (displayMode === "compact") return summary;
+ if (displayMode === "compact") {
+ return (
+
+
+
+
+ {summary}
+
+ );
+ }
const sessions = currentStreams.flatMap((result) =>
result.sessions.map((session) => ({ integrationId: result.integrationId, session })),
);
const audiobookStats = response.kind === "audiobookshelf" ? (response.data as AudiobookshelfDashboardData) : null;
const advancedGridColumns = width >= 760 ? 2 : 1;
+ const hasStreamErrors = currentStreams.some(({ error }) => Boolean(error));
return (
{summary}
+ {(statsError || streamsError || hasStreamErrors) && (
+
+
+
+
+
+ )}
{response.kind === "navidrome" ? (
@@ -66,15 +87,22 @@ export default function AudioStatsWidget({
if (!currentlyPlaying) return null;
return (
-
+
{currentlyPlaying.name}
- {currentlyPlaying.seasonName ?? currentlyPlaying.albumName ?? session.user.username}
+ {currentlyPlaying.seasonName ?? currentlyPlaying.albumName ?? session.user?.username ?? "—"}
-
+
{session.sessionName}
diff --git a/packages/widgets/src/smart-home/entity-state/component.tsx b/packages/widgets/src/smart-home/entity-state/component.tsx
index 94950135c..e3f776f66 100644
--- a/packages/widgets/src/smart-home/entity-state/component.tsx
+++ b/packages/widgets/src/smart-home/entity-state/component.tsx
@@ -50,7 +50,9 @@ export default function SmartHomeEntityStateWidget({
const apiUnit = entity?.attributes.unit_of_measurement;
const unit = options.entityUnit || (typeof apiUnit === "string" ? apiUnit : "");
const attribute = unit.length > 0 ? ` ${unit}` : "";
- const isActionable = options.clickable && canInteract && entity !== undefined && !isEntityPending && !isPending;
+ const isActionable =
+ options.clickable && canInteract && entity !== undefined && !entityError && !isEntityPending && !isPending;
+ const queryErrorLabel = entityError ? t("widget.smartHome-entityState.error.loadFailed") : undefined;
const handleClick = useCallback(() => {
if (isEditMode) {
@@ -108,7 +110,7 @@ export default function SmartHomeEntityStateWidget({
onClick={handleClick}
disabled={!isActionable}
aria-label={`${displayName}: ${state}${attribute}`}
- aria-description={error?.message}
+ aria-description={queryErrorLabel ?? (error ? t("widget.smartHome-entityState.error.toggleFailed") : undefined)}
w="100%"
h="100%"
styles={{
@@ -127,6 +129,11 @@ export default function SmartHomeEntityStateWidget({
{displayName}
+ {queryErrorLabel && (
+
+ {queryErrorLabel}
+
+ )}
{displayMode === "advanced" && entity && (
<>