diff --git a/apps/docs/docs/widgets/custom-api/index.mdx b/apps/docs/docs/widgets/custom-api/index.mdx
index 9325a86fe..4217f2369 100644
--- a/apps/docs/docs/widgets/custom-api/index.mdx
+++ b/apps/docs/docs/widgets/custom-api/index.mdx
@@ -18,7 +18,7 @@ Temporary inputs exist only while the tile is mounted. Homarr encrypts source cr
Homarr does not send these credentials to the browser. Homarr does not include them in an export.
-
+
### Adding the widget
diff --git a/packages/api/src/router/test/widgets/custom-api-v2.spec.ts b/packages/api/src/router/test/widgets/custom-api-v2.spec.ts
index a5e6f3c50..8bfc75319 100644
--- a/packages/api/src/router/test/widgets/custom-api-v2.spec.ts
+++ b/packages/api/src/router/test/widgets/custom-api-v2.spec.ts
@@ -84,7 +84,11 @@ const createSession = (userId: string): Session => ({
expires: new Date(Date.now() + 60_000).toISOString(),
});
-async function setup(isPublic = true) {
+async function setup(
+ isPublic = true,
+ customDefinition: HomarrCustomWidgetV2 = definition,
+ configuration: Record = {},
+) {
const db = createDb();
const ownerId = createId();
const boardId = createId();
@@ -94,14 +98,14 @@ async function setup(isPublic = true) {
await db.insert(boards).values({ id: boardId, name: createId(), creatorId: ownerId, isPublic });
await db.insert(customWidgetDefinitions).values({
id: definitionId,
- ...serializeCustomWidgetDefinition(definition),
+ ...serializeCustomWidgetDefinition(customDefinition),
creatorId: ownerId,
});
await db.insert(items).values({
id: itemId,
boardId,
kind: "customApi",
- options: stringifySuperJson({ definitionId, configuration: {}, configurationVersion: 1 }),
+ options: stringifySuperJson({ definitionId, configuration, configurationVersion: 1 }),
});
return { db, ownerId, boardId, itemId, definitionId };
}
@@ -122,6 +126,20 @@ describe("Custom JSX v2 board router", () => {
expect(mocks.executeRequest).toHaveBeenCalledOnce();
});
+ test("uses current defaults when an updated option no longer accepts the stored value", async () => {
+ const updatedDefinition: HomarrCustomWidgetV2 = {
+ ...definition,
+ options: {
+ limit: { label: "Limit", control: "slider", default: 5, min: 1, max: 10 },
+ },
+ template: "{options.limit}",
+ };
+ const { db, itemId } = await setup(true, updatedDefinition, { limit: "5", removed: true });
+ const caller = customApiRouter.createCaller({ db, deviceType: undefined, session: null });
+
+ await expect(caller.getData({ itemId })).resolves.toMatchObject({ options: { limit: 5 } });
+ });
+
test("hides private boards and disabled definitions before executing a request", async () => {
const privateSetup = await setup(false);
const anonymous = customApiRouter.createCaller({
diff --git a/packages/api/src/router/widgets/custom-api.ts b/packages/api/src/router/widgets/custom-api.ts
index 15954da00..778788e58 100644
--- a/packages/api/src/router/widgets/custom-api.ts
+++ b/packages/api/src/router/widgets/custom-api.ts
@@ -9,6 +9,7 @@ import type { BoardPermission } from "@homarr/definitions";
import {
getCustomWidgetConfirmation,
getCustomWidgetDefaultOptions,
+ normalizeCustomWidgetOptions,
validateCustomWidgetOptions,
} from "@homarr/custom-widgets/core";
import type { CustomJsxRequest, CustomWidgetSource } from "@homarr/custom-widgets/core";
@@ -96,7 +97,10 @@ async function resolvePlacedDefinitionAsync(ctx: RouterContext, itemId: string)
if (!stored.enabled) throw new TRPCError({ code: "FORBIDDEN", message: "Widget is disabled" });
const definition = parseStoredCustomWidgetDefinition(stored);
- const configuration = { ...getCustomWidgetDefaultOptions(definition.options), ...itemOptions.configuration };
+ const configuration =
+ itemOptions.configurationVersion === stored.updatedAt.getTime()
+ ? { ...getCustomWidgetDefaultOptions(definition.options), ...itemOptions.configuration }
+ : normalizeCustomWidgetOptions(definition.options, itemOptions.configuration);
const issues = validateCustomWidgetOptions(definition.options, configuration);
if (issues.length > 0) {
throw new TRPCError({
diff --git a/packages/custom-widgets/src/core/options.ts b/packages/custom-widgets/src/core/options.ts
index b6ca003b0..0cc292cba 100644
--- a/packages/custom-widgets/src/core/options.ts
+++ b/packages/custom-widgets/src/core/options.ts
@@ -12,6 +12,18 @@ export function getCustomWidgetDefaultOptions(
return Object.fromEntries(Object.entries(options).map(([name, option]) => [name, option.default]));
}
+export function normalizeCustomWidgetOptions(
+ options: CustomWidgetOptions,
+ value: Record,
+): Record {
+ return Object.fromEntries(
+ Object.entries(options).map(([name, option]) => [
+ name,
+ getCustomWidgetOptionValueIssue(option, value[name]) ? option.default : value[name],
+ ]),
+ );
+}
+
export function validateCustomWidgetOptions(
options: CustomWidgetOptions,
value: Record,
diff --git a/packages/custom-widgets/src/test/options.spec.ts b/packages/custom-widgets/src/test/options.spec.ts
index bd3eac0ef..078cfc62a 100644
--- a/packages/custom-widgets/src/test/options.spec.ts
+++ b/packages/custom-widgets/src/test/options.spec.ts
@@ -5,6 +5,7 @@ import {
customWidgetOptionControls,
customWidgetOptionSchema,
getCustomWidgetDefaultOptions,
+ normalizeCustomWidgetOptions,
validateCustomWidgetOptions,
} from "../core";
@@ -69,6 +70,29 @@ describe("lean Custom Widget options", () => {
expect(validateCustomWidgetOptions(options, configuration)).toEqual([]);
});
+ it("replaces stale values with current defaults", () => {
+ const options = customWidgetDefinitionSchema.parse({
+ $schema: "homarr-custom-widget-v2",
+ name: "Updated options",
+ sources: {},
+ requests: {},
+ options: {
+ limit: { label: "Limit", control: "slider", default: 5, min: 1, max: 10 },
+ label: { label: "Label", control: "text", default: "Default" },
+ },
+ template: "{options.limit}",
+ }).options;
+
+ const normalized = normalizeCustomWidgetOptions(options, {
+ limit: "5",
+ label: "Custom",
+ removed: true,
+ });
+
+ expect(normalized).toEqual({ limit: 5, label: "Custom" });
+ expect(validateCustomWidgetOptions(options, normalized)).toEqual([]);
+ });
+
it("rejects duplicate static choice values", () => {
expect(
customWidgetOptionSchema.safeParse({
diff --git a/packages/widgets/src/_inputs/widget-custom-widget-configuration-input.tsx b/packages/widgets/src/_inputs/widget-custom-widget-configuration-input.tsx
index 052563fee..619d8e0bc 100644
--- a/packages/widgets/src/_inputs/widget-custom-widget-configuration-input.tsx
+++ b/packages/widgets/src/_inputs/widget-custom-widget-configuration-input.tsx
@@ -21,7 +21,7 @@ import { IconAlertTriangle } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useOptionalBoard } from "@homarr/boards/context";
import type { CustomWidgetOption } from "@homarr/custom-widgets/core";
-import { validateCustomWidgetOptions } from "@homarr/custom-widgets/core";
+import { normalizeCustomWidgetOptions, validateCustomWidgetOptions } from "@homarr/custom-widgets/core";
import { CustomWidgetCodeEditor } from "@homarr/custom-widgets/workbench";
import type { CustomWidgetEditorMessages } from "@homarr/custom-widgets/workbench";
import { IconPicker } from "@homarr/forms-collection";
@@ -45,10 +45,23 @@ export const WidgetCustomWidgetConfigurationInput = ({
);
const definition = available.data?.find((candidate) => candidate.id === definitionId);
const options = isRecord(definition?.options) ? (definition.options as Record) : null;
- const issues = useMemo(
- () => (options ? validateCustomWidgetOptions(options, configuration) : []),
- [configuration, options],
+ const definitionVersion = definition?.updatedAt instanceof Date ? definition.updatedAt.getTime() : undefined;
+ const configurationVersion = form.values.options.configurationVersion;
+ const needsDefinitionRepair = definitionVersion !== undefined && configurationVersion !== definitionVersion;
+ const effectiveConfiguration = useMemo(
+ () => (options && needsDefinitionRepair ? normalizeCustomWidgetOptions(options, configuration) : configuration),
+ [configuration, needsDefinitionRepair, options],
);
+ const issues = useMemo(
+ () => (options ? validateCustomWidgetOptions(options, effectiveConfiguration) : []),
+ [effectiveConfiguration, options],
+ );
+
+ useEffect(() => {
+ if (!needsDefinitionRepair || definitionVersion === undefined) return;
+ form.setFieldValue(`options.${property}`, effectiveConfiguration);
+ form.setFieldValue("options.configurationVersion", definitionVersion);
+ }, [definitionVersion, effectiveConfiguration, form, needsDefinitionRepair, property]);
useEffect(() => {
const path = `options.${property}`;
@@ -72,7 +85,7 @@ export const WidgetCustomWidgetConfigurationInput = ({
key={`${definitionId}:${name}`}
option={option}
path={`options.${property}.${name}`}
- configuration={configuration}
+ configuration={effectiveConfiguration}
definitionId={definitionId}
/>
))}
@@ -87,7 +100,7 @@ export const WidgetCustomWidgetConfigurationInput = ({
key={`${definitionId}:${name}`}
option={option}
path={`options.${property}.${name}`}
- configuration={configuration}
+ configuration={effectiveConfiguration}
definitionId={definitionId}
/>
))}
diff --git a/packages/widgets/src/advanced-focus-support.spec.ts b/packages/widgets/src/advanced-focus-support.spec.ts
index 2ee1dcc84..5e69c39fb 100644
--- a/packages/widgets/src/advanced-focus-support.spec.ts
+++ b/packages/widgets/src/advanced-focus-support.spec.ts
@@ -3,41 +3,44 @@ import { describe, expect, test } from "vitest";
import { supportsAdvancedFocus } from "./definition";
import { widgetImports } from ".";
-const compactOnlyWidgetKinds = [
- "app",
- "bazarr",
- "beszelAlerts",
- "beszelSystemGrid",
- "beszelSystemStats",
- "beszelSystemTable",
- "coolify",
- "dnsHoleControls",
- "dnsHoleSummary",
- "iframe",
- "indexerManager",
- "mediaRequests-requestList",
- "mediaRequests-requestStats",
- "minecraftServerStatus",
- "networkControllerSummary",
- "notebook",
- "notifications",
- "paperlessNgx",
- "patchmon",
- "releases",
- "rssFeed",
- "stockPrice",
- "systemDisks",
- "video",
- "vpn",
+const advancedFocusWidgetKinds = [
+ "anchorNote",
+ "archiveTeamWarrior",
+ "audioStats",
+ "bookmarks",
+ "calendar",
+ "clock",
+ "dockerContainers",
+ "downloads",
+ "firewall",
+ "healthMonitoring",
+ "immich-albumCarousel",
+ "immich-serverStats",
+ "mediaMissing",
+ "mediaReleases",
+ "mediaServer",
+ "mediaTranscoding",
+ "networkControllerStatus",
+ "smartHome-entityState",
+ "smartHome-executeAutomation",
+ "speedtestTracker",
+ "systemResources",
+ "timetable",
+ "tracearr",
+ "traefik",
+ "umami",
+ "ups",
+ "uptimeKuma",
+ "weather",
] as const;
describe("advanced focus support", () => {
test("is reserved for widgets with a distinct advanced experience", () => {
- const actualCompactOnlyKinds = Object.entries(widgetImports)
- .filter(([, widgetImport]) => !supportsAdvancedFocus(widgetImport.definition))
+ const actualAdvancedFocusKinds = Object.entries(widgetImports)
+ .filter(([, widgetImport]) => supportsAdvancedFocus(widgetImport.definition))
.map(([kind]) => kind)
.toSorted();
- expect(actualCompactOnlyKinds).toEqual([...compactOnlyWidgetKinds].toSorted());
+ expect(actualAdvancedFocusKinds).toEqual([...advancedFocusWidgetKinds].toSorted());
});
});
diff --git a/packages/widgets/src/anchor-note/index.ts b/packages/widgets/src/anchor-note/index.ts
index 9baaccf2d..4eac1a441 100644
--- a/packages/widgets/src/anchor-note/index.ts
+++ b/packages/widgets/src/anchor-note/index.ts
@@ -5,6 +5,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("anchorNote", {
icon: IconNotes,
+ supportsAdvancedFocus: true,
queryKey: [["widget", "anchorNotes"]],
maxIntegrations: 1,
supportedIntegrations: ["anchor"],
diff --git a/packages/widgets/src/archive-team-warrior/index.ts b/packages/widgets/src/archive-team-warrior/index.ts
index 1eaf0a691..38a3eef36 100644
--- a/packages/widgets/src/archive-team-warrior/index.ts
+++ b/packages/widgets/src/archive-team-warrior/index.ts
@@ -5,6 +5,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("archiveTeamWarrior", {
icon: IconArchive,
+ supportsAdvancedFocus: true,
maxIntegrations: 1,
createOptions() {
return optionsBuilder.from((factory) => ({
diff --git a/packages/widgets/src/audio-stats/index.ts b/packages/widgets/src/audio-stats/index.ts
index a21e70278..6fb3b2014 100644
--- a/packages/widgets/src/audio-stats/index.ts
+++ b/packages/widgets/src/audio-stats/index.ts
@@ -13,6 +13,7 @@ const hideUnlessAudiobookshelf = {
export const { definition, componentLoader } = createWidgetDefinition("audioStats", {
icon: IconHeadphones,
+ supportsAdvancedFocus: true,
queryKeys: [[["widget", "audioStats", "getStats"]], [["widget", "mediaServer", "getCurrentStreams"]]],
queryMatcher: matchesWidgetRuntimeQuery,
supportedIntegrations: ["navidrome", "audiobookshelf"],
diff --git a/packages/widgets/src/bookmarks/index.tsx b/packages/widgets/src/bookmarks/index.tsx
index c18ff796e..85fc442cf 100644
--- a/packages/widgets/src/bookmarks/index.tsx
+++ b/packages/widgets/src/bookmarks/index.tsx
@@ -10,6 +10,7 @@ import { BookmarkAddButton } from "./add-button";
export const { definition, componentLoader } = createWidgetDefinition("bookmarks", {
icon: IconBookmark,
+ supportsAdvancedFocus: true,
queryKey: [["app", "byIds"]],
createOptions() {
return optionsBuilder.from((factory) => ({
diff --git a/packages/widgets/src/calendar/index.ts b/packages/widgets/src/calendar/index.ts
index 270981f5d..471c50f17 100644
--- a/packages/widgets/src/calendar/index.ts
+++ b/packages/widgets/src/calendar/index.ts
@@ -9,6 +9,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("calendar", {
icon: IconCalendar,
+ supportsAdvancedFocus: true,
queryKey: [["widget", "calendar", "findAllEvents"]],
queryMatcher: matchesWidgetRuntimeQuery,
createOptions() {
diff --git a/packages/widgets/src/clock/index.ts b/packages/widgets/src/clock/index.ts
index e7657b918..45b040009 100644
--- a/packages/widgets/src/clock/index.ts
+++ b/packages/widgets/src/clock/index.ts
@@ -6,6 +6,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("clock", {
icon: IconClock,
+ supportsAdvancedFocus: true,
queryKey: [["widget", "weather", "atLocation"]],
queryMatcher: ({ input }, scope) => {
const location = scope.options.weatherLocation;
diff --git a/packages/widgets/src/definition.spec.ts b/packages/widgets/src/definition.spec.ts
index 7f976692c..215c884ef 100644
--- a/packages/widgets/src/definition.spec.ts
+++ b/packages/widgets/src/definition.spec.ts
@@ -2,7 +2,9 @@ import { describe, expect, test } from "vitest";
import { definition as appDefinition } from "./app";
import { definition as beszelSystemGridDefinition } from "./beszel-system-grid";
import { definition as beszelSystemTableDefinition } from "./beszel-system-table";
+import { definition as customApiDefinition } from "./custom-api";
import { getWidgetQueryKeys, supportsAdvancedFocus } from "./definition";
+import { definition as downloadsDefinition } from "./downloads";
describe("getWidgetQueryKeys", () => {
test("falls back to the widget router namespace", () => {
@@ -24,10 +26,15 @@ describe("getWidgetQueryKeys", () => {
});
describe("supportsAdvancedFocus", () => {
- test.each([appDefinition, beszelSystemGridDefinition, beszelSystemTableDefinition])(
+ test.each([appDefinition, beszelSystemGridDefinition, beszelSystemTableDefinition, customApiDefinition])(
"keeps non-enhanced widgets compact",
(widgetDefinition) => {
expect(supportsAdvancedFocus(widgetDefinition)).toBe(false);
},
);
+
+ test("requires an explicit opt-in", () => {
+ expect(supportsAdvancedFocus({})).toBe(false);
+ expect(supportsAdvancedFocus(downloadsDefinition)).toBe(true);
+ });
});
diff --git a/packages/widgets/src/definition.ts b/packages/widgets/src/definition.ts
index 3f148ce77..6ef443999 100644
--- a/packages/widgets/src/definition.ts
+++ b/packages/widgets/src/definition.ts
@@ -127,7 +127,7 @@ export interface WidgetDefinition {
}
export const supportsAdvancedFocus = (definition: object) =>
- !("supportsAdvancedFocus" in definition) || definition.supportsAdvancedFocus !== false;
+ "supportsAdvancedFocus" in definition && definition.supportsAdvancedFocus === true;
export const getWidgetQueryKeys = (
definition: {
diff --git a/packages/widgets/src/docker/index.ts b/packages/widgets/src/docker/index.ts
index bc229fed5..2ca930913 100644
--- a/packages/widgets/src/docker/index.ts
+++ b/packages/widgets/src/docker/index.ts
@@ -25,6 +25,7 @@ const columnTranslationKeyMap = {
export const { definition, componentLoader } = createWidgetDefinition("dockerContainers", {
icon: IconBrandDocker,
+ supportsAdvancedFocus: true,
queryKey: [["docker", "getContainers"]],
refetchInterval: 30,
createOptions() {
diff --git a/packages/widgets/src/downloads/index.ts b/packages/widgets/src/downloads/index.ts
index 60064de9a..7ac9d1182 100644
--- a/packages/widgets/src/downloads/index.ts
+++ b/packages/widgets/src/downloads/index.ts
@@ -42,6 +42,7 @@ const sortColumns = [
export const { definition, componentLoader } = createWidgetDefinition("downloads", {
icon: IconDownload,
+ supportsAdvancedFocus: true,
queryKey: [["widget", "downloads", "getJobsAndStatuses"]],
queryMatcher: ({ input }, scope) =>
widgetQueryInputMatches(input, {
diff --git a/packages/widgets/src/firewall/index.ts b/packages/widgets/src/firewall/index.ts
index e154fd3ac..2a6b24854 100644
--- a/packages/widgets/src/firewall/index.ts
+++ b/packages/widgets/src/firewall/index.ts
@@ -7,6 +7,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("firewall", {
icon: IconWall,
+ supportsAdvancedFocus: true,
refetchInterval: 5,
createOptions() {
return optionsBuilder.from(() => ({}));
diff --git a/packages/widgets/src/health-monitoring/index.ts b/packages/widgets/src/health-monitoring/index.ts
index c322e322a..1b2e84982 100644
--- a/packages/widgets/src/health-monitoring/index.ts
+++ b/packages/widgets/src/health-monitoring/index.ts
@@ -8,6 +8,7 @@ import { createStorageVolumeMultiSelectOptions } from "../storage-volume-options
export const { definition, componentLoader } = createWidgetDefinition("healthMonitoring", {
icon: IconHeartRateMonitor,
+ supportsAdvancedFocus: true,
queryKeys: [[["integration", "byIds"]], [["widget", "healthMonitoring"]]],
refetchInterval: 5,
createOptions() {
diff --git a/packages/widgets/src/immich/album-carousel/index.ts b/packages/widgets/src/immich/album-carousel/index.ts
index f18585280..34eb0e667 100644
--- a/packages/widgets/src/immich/album-carousel/index.ts
+++ b/packages/widgets/src/immich/album-carousel/index.ts
@@ -51,6 +51,7 @@ const createOptions = () =>
export const { definition, componentLoader } = createWidgetDefinition("immich-albumCarousel", {
icon: IconPhoto,
+ supportsAdvancedFocus: true,
queryKey: [["widget", "immich", "getAlbum"]],
queryMatcher: ({ input }, scope) =>
widgetQueryInputMatches(input, {
diff --git a/packages/widgets/src/immich/server-stats/index.ts b/packages/widgets/src/immich/server-stats/index.ts
index a1c966664..dedd420fe 100644
--- a/packages/widgets/src/immich/server-stats/index.ts
+++ b/packages/widgets/src/immich/server-stats/index.ts
@@ -5,6 +5,7 @@ import { optionsBuilder } from "../../options";
export const { definition, componentLoader } = createWidgetDefinition("immich-serverStats", {
icon: IconGraphFilled,
+ supportsAdvancedFocus: true,
queryKeys: [[["widget", "immich", "getServerStats"]], [["widget", "immich", "getAlbums"]]],
queryMatcher: matchesWidgetRuntimeQuery,
refetchInterval: null,
diff --git a/packages/widgets/src/media-missing/index.ts b/packages/widgets/src/media-missing/index.ts
index 69a6ecf73..38ac8cd68 100644
--- a/packages/widgets/src/media-missing/index.ts
+++ b/packages/widgets/src/media-missing/index.ts
@@ -9,6 +9,7 @@ const pageSizeOptions = ["10", "20", "30", "50"] as const;
export const { componentLoader, definition } = createWidgetDefinition("mediaMissing", {
icon: IconMovie,
+ supportsAdvancedFocus: true,
queryKey: [["widget", "mediaOrganizer", "getData"]],
queryMatcher: matchesWidgetRuntimeQuery,
createOptions() {
diff --git a/packages/widgets/src/media-releases/index.ts b/packages/widgets/src/media-releases/index.ts
index 3e6cccbe5..55eff84e5 100644
--- a/packages/widgets/src/media-releases/index.ts
+++ b/packages/widgets/src/media-releases/index.ts
@@ -5,6 +5,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("mediaReleases", {
icon: IconTicket,
+ supportsAdvancedFocus: true,
queryKey: [["widget", "mediaRelease"]],
refetchInterval: null,
createOptions() {
diff --git a/packages/widgets/src/media-server/index.ts b/packages/widgets/src/media-server/index.ts
index 46f3fd489..559b029ee 100644
--- a/packages/widgets/src/media-server/index.ts
+++ b/packages/widgets/src/media-server/index.ts
@@ -7,6 +7,7 @@ import { optionsBuilder } from "../options";
export const { componentLoader, definition } = createWidgetDefinition("mediaServer", {
icon: IconVideo,
+ supportsAdvancedFocus: true,
queryKey: [["widget", "mediaServer", "getCurrentStreams"]],
queryMatcher: ({ input }, scope) =>
widgetQueryInputMatches(input, {
diff --git a/packages/widgets/src/media-transcoding/index.ts b/packages/widgets/src/media-transcoding/index.ts
index fc91622e3..bff9c1ea3 100644
--- a/packages/widgets/src/media-transcoding/index.ts
+++ b/packages/widgets/src/media-transcoding/index.ts
@@ -11,6 +11,7 @@ export const views = ["workers", "queue", "statistics"] as const;
export const { componentLoader, definition } = createWidgetDefinition("mediaTranscoding", {
icon: IconTransform,
+ supportsAdvancedFocus: true,
queryKey: [["widget", "mediaTranscoding", "getDataAsync"]],
queryMatcher: matchesWidgetRuntimeQuery,
maxIntegrations: 1,
diff --git a/packages/widgets/src/network-controller/network-status/index.ts b/packages/widgets/src/network-controller/network-status/index.ts
index 3ab66da82..e3741d8e1 100644
--- a/packages/widgets/src/network-controller/network-status/index.ts
+++ b/packages/widgets/src/network-controller/network-status/index.ts
@@ -7,6 +7,7 @@ import { optionsBuilder } from "../../options";
export const { definition, componentLoader } = createWidgetDefinition("networkControllerStatus", {
icon: IconTopologyFull,
+ supportsAdvancedFocus: true,
queryKey: [["widget", "networkController"]],
refetchInterval: null,
createOptions() {
diff --git a/packages/widgets/src/smart-home/entity-state/index.ts b/packages/widgets/src/smart-home/entity-state/index.ts
index 93a12a872..551fa42de 100644
--- a/packages/widgets/src/smart-home/entity-state/index.ts
+++ b/packages/widgets/src/smart-home/entity-state/index.ts
@@ -7,6 +7,7 @@ import { optionsBuilder } from "../../options";
export const { definition, componentLoader } = createWidgetDefinition("smartHome-entityState", {
icon: IconBinaryTree,
+ supportsAdvancedFocus: true,
queryKey: [["widget", "smartHome"]],
maxIntegrations: 1,
createOptions() {
diff --git a/packages/widgets/src/smart-home/execute-automation/index.ts b/packages/widgets/src/smart-home/execute-automation/index.ts
index e4435f892..2a349a84e 100644
--- a/packages/widgets/src/smart-home/execute-automation/index.ts
+++ b/packages/widgets/src/smart-home/execute-automation/index.ts
@@ -7,6 +7,7 @@ import { optionsBuilder } from "../../options";
export const { definition, componentLoader } = createWidgetDefinition("smartHome-executeAutomation", {
icon: IconBinaryTree,
+ supportsAdvancedFocus: true,
maxIntegrations: 1,
createOptions() {
return optionsBuilder.from((factory) => ({
diff --git a/packages/widgets/src/speedtest-tracker/index.ts b/packages/widgets/src/speedtest-tracker/index.ts
index 14cc5d118..7de41100f 100644
--- a/packages/widgets/src/speedtest-tracker/index.ts
+++ b/packages/widgets/src/speedtest-tracker/index.ts
@@ -5,6 +5,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("speedtestTracker", {
icon: IconSpeedboat,
+ supportsAdvancedFocus: true,
refetchInterval: null,
createOptions() {
return optionsBuilder.from(
diff --git a/packages/widgets/src/system-resources/index.ts b/packages/widgets/src/system-resources/index.ts
index b35bab42f..7c0e84fdc 100644
--- a/packages/widgets/src/system-resources/index.ts
+++ b/packages/widgets/src/system-resources/index.ts
@@ -14,6 +14,7 @@ const labelDisplayModeOptions = {
export const { definition, componentLoader } = createWidgetDefinition("systemResources", {
icon: IconGraphFilled,
+ supportsAdvancedFocus: true,
queryKey: [["widget", "healthMonitoring"]],
refetchInterval: 5,
supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid", "glances", "synology"],
diff --git a/packages/widgets/src/timetable/index.ts b/packages/widgets/src/timetable/index.ts
index 81ec81db8..acd3f703d 100644
--- a/packages/widgets/src/timetable/index.ts
+++ b/packages/widgets/src/timetable/index.ts
@@ -9,6 +9,7 @@ import { optionsBuilder } from "../options";
export const { componentLoader, definition } = createWidgetDefinition("timetable", {
icon: IconBusStop,
+ supportsAdvancedFocus: true,
errors: {
BAD_REQUEST: {
icon: IconWorldOff,
diff --git a/packages/widgets/src/tracearr/index.ts b/packages/widgets/src/tracearr/index.ts
index 9c34eb548..13851a846 100644
--- a/packages/widgets/src/tracearr/index.ts
+++ b/packages/widgets/src/tracearr/index.ts
@@ -5,6 +5,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("tracearr", {
icon: IconActivityHeartbeat,
+ supportsAdvancedFocus: true,
refetchInterval: 5,
createOptions() {
return optionsBuilder.from((factory) => ({
diff --git a/packages/widgets/src/traefik/index.ts b/packages/widgets/src/traefik/index.ts
index 854075121..95392a152 100644
--- a/packages/widgets/src/traefik/index.ts
+++ b/packages/widgets/src/traefik/index.ts
@@ -5,6 +5,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("traefik", {
icon: IconRoute,
+ supportsAdvancedFocus: true,
createOptions() {
return optionsBuilder.from((factory) => ({
showTcp: factory.switch({
diff --git a/packages/widgets/src/umami/index.ts b/packages/widgets/src/umami/index.ts
index 423eebebe..9c96e4a8e 100644
--- a/packages/widgets/src/umami/index.ts
+++ b/packages/widgets/src/umami/index.ts
@@ -8,6 +8,7 @@ export const timeFrameValues = ["today", "24h", "7d", "30d", "month", "lastMonth
export const { definition, componentLoader } = createWidgetDefinition("umami", {
icon: IconChartBar,
+ supportsAdvancedFocus: true,
queryKeys: [
[["widget", "umami", "getVisitorStats"]],
[["widget", "umami", "getActiveVisitors"]],
diff --git a/packages/widgets/src/ups/index.ts b/packages/widgets/src/ups/index.ts
index 681daf417..0ee8c70d5 100644
--- a/packages/widgets/src/ups/index.ts
+++ b/packages/widgets/src/ups/index.ts
@@ -7,6 +7,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("ups", {
icon: IconBatteryCharging,
+ supportsAdvancedFocus: true,
createOptions() {
return optionsBuilder.from((factory) => ({
showBattery: factory.switch({
diff --git a/packages/widgets/src/uptime-kuma/index.ts b/packages/widgets/src/uptime-kuma/index.ts
index b9eebcd8c..0b0cff5ad 100644
--- a/packages/widgets/src/uptime-kuma/index.ts
+++ b/packages/widgets/src/uptime-kuma/index.ts
@@ -5,6 +5,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("uptimeKuma", {
icon: IconHeartbeat,
+ supportsAdvancedFocus: true,
createOptions() {
return optionsBuilder.from((factory) => ({
showAverageUptime: factory.switch({
diff --git a/packages/widgets/src/weather/index.ts b/packages/widgets/src/weather/index.ts
index 978c9a855..11ced3b39 100644
--- a/packages/widgets/src/weather/index.ts
+++ b/packages/widgets/src/weather/index.ts
@@ -7,6 +7,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("weather", {
icon: IconCloud,
+ supportsAdvancedFocus: true,
queryKey: [["widget", "weather", "atLocation"]],
queryMatcher: ({ input }, scope) => {
const location = scope.options.location;