From d513c7fe464783e2609287afe2a74edababfe310 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Sun, 9 Aug 2026 09:17:32 +0200 Subject: [PATCH 1/2] fix: repair stale custom widget options --- .../router/test/widgets/custom-api-v2.spec.ts | 24 +++++++++++++++--- packages/api/src/router/widgets/custom-api.ts | 6 ++++- packages/custom-widgets/src/core/options.ts | 12 +++++++++ .../custom-widgets/src/test/options.spec.ts | 24 ++++++++++++++++++ ...dget-custom-widget-configuration-input.tsx | 25 ++++++++++++++----- 5 files changed, 81 insertions(+), 10 deletions(-) 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 ac8462a73..d77125f90 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"; @@ -97,7 +98,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} /> ))} From f214c25e9cf1b83cac1e0f4a799c21ace6cbe070 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Sun, 9 Aug 2026 09:19:54 +0200 Subject: [PATCH 2/2] fix: require advanced widget focus opt-in --- apps/docs/docs/widgets/custom-api/index.mdx | 2 +- .../src/advanced-focus-support.spec.ts | 61 ++++++++++--------- packages/widgets/src/anchor-note/index.ts | 1 + .../widgets/src/archive-team-warrior/index.ts | 1 + packages/widgets/src/audio-stats/index.ts | 1 + packages/widgets/src/bookmarks/index.tsx | 1 + packages/widgets/src/calendar/index.ts | 1 + packages/widgets/src/clock/index.ts | 1 + packages/widgets/src/definition.spec.ts | 9 ++- packages/widgets/src/definition.ts | 2 +- packages/widgets/src/docker/index.ts | 1 + packages/widgets/src/downloads/index.ts | 1 + packages/widgets/src/firewall/index.ts | 1 + .../widgets/src/health-monitoring/index.ts | 1 + .../src/immich/album-carousel/index.ts | 1 + .../widgets/src/immich/server-stats/index.ts | 1 + packages/widgets/src/media-missing/index.ts | 1 + packages/widgets/src/media-releases/index.ts | 1 + packages/widgets/src/media-server/index.ts | 1 + .../widgets/src/media-transcoding/index.ts | 1 + .../network-status/index.ts | 1 + .../src/smart-home/entity-state/index.ts | 1 + .../smart-home/execute-automation/index.ts | 1 + .../widgets/src/speedtest-tracker/index.ts | 1 + .../widgets/src/system-resources/index.ts | 1 + packages/widgets/src/timetable/index.ts | 1 + packages/widgets/src/tracearr/index.ts | 1 + packages/widgets/src/traefik/index.ts | 1 + packages/widgets/src/umami/index.ts | 1 + packages/widgets/src/ups/index.ts | 1 + packages/widgets/src/uptime-kuma/index.ts | 1 + packages/widgets/src/weather/index.ts | 1 + 32 files changed, 70 insertions(+), 32 deletions(-) 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/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;