diff --git a/apps/docs/docs/advanced/command-line/index.mdx b/apps/docs/docs/advanced/command-line/index.mdx index 04024254f..0e77162a2 100644 --- a/apps/docs/docs/advanced/command-line/index.mdx +++ b/apps/docs/docs/advanced/command-line/index.mdx @@ -36,6 +36,8 @@ pnpm cli harness setup feature-board The command builds `homarr:feature-board`, starts a container with a persistent data volume, enables demo mode, and keeps demo mutations writable (`DEMO_READ_ONLY=false`). Docker assigns a free host port by default; use the URL printed by the command or query it later: +The demo board includes every widget kind. Integration-backed widgets use the bundled mock integration; media widgets use public stock assets. + ```sh pnpm cli harness port feature-board ``` diff --git a/packages/api/src/router/widgets/anchor-notes.ts b/packages/api/src/router/widgets/anchor-notes.ts index d431c0de9..790c957c5 100644 --- a/packages/api/src/router/widgets/anchor-notes.ts +++ b/packages/api/src/router/widgets/anchor-notes.ts @@ -3,7 +3,12 @@ import { z } from "zod/v4"; import { ResponseError } from "@homarr/common/server"; import { createLogger } from "@homarr/core/infrastructure/logs"; -import { anchorNotesListInputSchema, anchorNoteUpdateInputSchema, createIntegrationAsync } from "@homarr/integrations"; +import { + anchorNotesListInputSchema, + anchorNoteUpdateInputSchema, + createIntegrationAsync, + mockWidgetData, +} from "@homarr/integrations"; import { anchorNoteRequestHandler, anchorNotesListRequestHandler } from "@homarr/request-handler/anchor-notes"; import { createOneIntegrationMiddleware } from "../../middlewares/integration"; @@ -15,6 +20,12 @@ const noteIdInput = z.object({ const logger = createLogger({ module: "anchorNotesRouter" }); +const assertMockNoteExists = (noteId: string) => { + if (noteId === mockWidgetData.anchorNote.id) return; + + throw new TRPCError({ code: "NOT_FOUND", message: "Note not found" }); +}; + const isJsonDeltaString = (value: string) => { try { const parsed: unknown = JSON.parse(value); @@ -40,34 +51,64 @@ const normalizeAnchorContent = (content: string | undefined) => { export const anchorNotesRouter = createTRPCRouter({ listNotes: publicProcedure - .concat(createOneIntegrationMiddleware("query", "anchor")) + .concat(createOneIntegrationMiddleware("query", "anchor", "mock")) .input(anchorNotesListInputSchema) .query(async ({ ctx, input }) => { - const handler = anchorNotesListRequestHandler.handler(ctx.integration, { ...input, limit: input.limit ?? 50 }); + if (ctx.integration.kind === "mock") { + const search = input.search?.toLowerCase(); + const notes = mockWidgetData.anchorNotes.filter((note) => !search || note.title.toLowerCase().includes(search)); + return notes.slice(0, input.limit ?? 50); + } + + const handler = anchorNotesListRequestHandler.handler( + { ...ctx.integration, kind: "anchor" }, + { + ...input, + limit: input.limit ?? 50, + }, + ); const { data } = await handler.getDataAsync(); return data; }), getNote: publicProcedure - .concat(createOneIntegrationMiddleware("query", "anchor")) + .concat(createOneIntegrationMiddleware("query", "anchor", "mock")) .input(noteIdInput) .query(async ({ ctx, input }) => { - const handler = anchorNoteRequestHandler.handler(ctx.integration, { noteId: input.noteId }); + if (ctx.integration.kind === "mock") { + assertMockNoteExists(input.noteId); + return mockWidgetData.anchorNote; + } + + const handler = anchorNoteRequestHandler.handler( + { ...ctx.integration, kind: "anchor" }, + { noteId: input.noteId }, + ); const { data } = await handler.getDataAsync(); return data; }), updateNote: protectedProcedure - .concat(createOneIntegrationMiddleware("interact", "anchor")) + .concat(createOneIntegrationMiddleware("interact", "anchor", "mock")) .input(anchorNoteUpdateInputSchema) .mutation(async ({ ctx, input }) => { - const integrationInstance = await createIntegrationAsync(ctx.integration); const normalizedContent = normalizeAnchorContent(input.content); const isContentNormalized = input.content !== undefined && normalizedContent !== undefined && input.content !== normalizedContent; + if (ctx.integration.kind === "mock") { + assertMockNoteExists(input.noteId); + return { + ...mockWidgetData.anchorNote, + ...(input.title !== undefined ? { title: input.title } : {}), + ...(normalizedContent !== undefined ? { content: normalizedContent } : {}), + }; + } + + const integrationInstance = await createIntegrationAsync({ ...ctx.integration, kind: "anchor" }); + try { const updatedNote = await integrationInstance.updateNoteAsync({ noteId: input.noteId, diff --git a/packages/api/src/router/widgets/archive-team-warrior.ts b/packages/api/src/router/widgets/archive-team-warrior.ts index d71b7546a..9fa96b553 100644 --- a/packages/api/src/router/widgets/archive-team-warrior.ts +++ b/packages/api/src/router/widgets/archive-team-warrior.ts @@ -1,13 +1,18 @@ import { archiveTeamWarriorRequestHandler } from "@homarr/request-handler/archive-team-warrior"; +import { mockWidgetData } from "@homarr/integrations"; import { createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createTRPCRouter, publicProcedure } from "../../trpc"; export const archiveTeamWarriorRouter = createTRPCRouter({ getStatus: publicProcedure - .concat(createOneIntegrationMiddleware("query", "archiveTeamWarrior")) + .concat(createOneIntegrationMiddleware("query", "archiveTeamWarrior", "mock")) .query(async ({ ctx }) => { - const handler = archiveTeamWarriorRequestHandler.handler(ctx.integration, {}); + if (ctx.integration.kind === "mock") { + return { status: mockWidgetData.archiveTeamWarrior, updatedAt: new Date(mockWidgetData.timestamp) }; + } + + const handler = archiveTeamWarriorRequestHandler.handler({ ...ctx.integration, kind: "archiveTeamWarrior" }, {}); const { data, timestamp } = await handler.getDataAsync(); return { diff --git a/packages/api/src/router/widgets/audio-stats.ts b/packages/api/src/router/widgets/audio-stats.ts index 14fd7d49c..cc3e25ddd 100644 --- a/packages/api/src/router/widgets/audio-stats.ts +++ b/packages/api/src/router/widgets/audio-stats.ts @@ -1,14 +1,16 @@ import { audiobookshelfRequestHandler } from "@homarr/request-handler/audiobookshelf"; import { navidromeRequestHandler } from "@homarr/request-handler/navidrome"; +import { mockWidgetData } from "@homarr/integrations"; import { createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createTRPCRouter, publicProcedure } from "../../trpc"; export const audioStatsRouter = createTRPCRouter({ getStats: publicProcedure - .concat(createOneIntegrationMiddleware("query", "navidrome", "audiobookshelf")) + .concat(createOneIntegrationMiddleware("query", "navidrome", "audiobookshelf", "mock")) .query(async ({ ctx }) => { const { kind } = ctx.integration; + if (kind === "mock") return { kind: "navidrome" as const, data: mockWidgetData.audioStats }; const fetchByKind = { navidrome: async () => { diff --git a/packages/api/src/router/widgets/bazarr.ts b/packages/api/src/router/widgets/bazarr.ts index c47ad4173..e9f8093b5 100644 --- a/packages/api/src/router/widgets/bazarr.ts +++ b/packages/api/src/router/widgets/bazarr.ts @@ -1,4 +1,5 @@ import { bazarrBadgesRequestHandler } from "@homarr/request-handler/bazarr"; +import { mockWidgetData } from "@homarr/integrations"; import { createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createTRPCRouter, publicProcedure } from "../../trpc"; @@ -12,9 +13,11 @@ export const bazarrRouter = createTRPCRouter({ "Get missing subtitle counts, provider issues, and health warnings for a Bazarr integration. REQUIRED: integrationId from integration_all", }, }) - .concat(createOneIntegrationMiddleware("query", "bazarr")) + .concat(createOneIntegrationMiddleware("query", "bazarr", "mock")) .query(async ({ ctx }) => { - const innerHandler = bazarrBadgesRequestHandler.handler(ctx.integration, {}); + if (ctx.integration.kind === "mock") return mockWidgetData.bazarr; + + const innerHandler = bazarrBadgesRequestHandler.handler({ ...ctx.integration, kind: "bazarr" }, {}); const data = await innerHandler.getDataAsync(); return data.data; }), diff --git a/packages/api/src/router/widgets/coolify.ts b/packages/api/src/router/widgets/coolify.ts index 133d36f14..d0d8b8355 100644 --- a/packages/api/src/router/widgets/coolify.ts +++ b/packages/api/src/router/widgets/coolify.ts @@ -1,4 +1,5 @@ import type { CoolifyInstanceInfo } from "@homarr/integrations/types"; +import { mockWidgetData } from "@homarr/integrations"; import { coolifyRequestHandler } from "@homarr/request-handler/coolify"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; @@ -16,12 +17,22 @@ interface CoolifyInstanceResult { export const coolifyRouter = createTRPCRouter({ getInstancesInfo: publicProcedure - .concat(createManyIntegrationMiddleware("query", "coolify")) + .concat(createManyIntegrationMiddleware("query", "coolify", "mock")) .query(async ({ ctx }) => { return await settleIntegrationQueries( ctx.integrations, async (integration): Promise => { - const innerHandler = coolifyRequestHandler.handler(integration, {}); + if (integration.kind === "mock") { + return { + integrationId: integration.id, + integrationName: integration.name, + integrationUrl: integration.url, + instanceInfo: mockWidgetData.coolify, + updatedAt: new Date(mockWidgetData.timestamp), + }; + } + + const innerHandler = coolifyRequestHandler.handler({ ...integration, kind: "coolify" }, {}); const { data, timestamp } = await innerHandler.getDataAsync(); return { diff --git a/packages/api/src/router/widgets/firewall.ts b/packages/api/src/router/widgets/firewall.ts index 335f75d09..28888649f 100644 --- a/packages/api/src/router/widgets/firewall.ts +++ b/packages/api/src/router/widgets/firewall.ts @@ -1,4 +1,6 @@ import { getIntegrationKindsByCategory } from "@homarr/definitions"; +import type { IntegrationKind } from "@homarr/definitions"; +import { mockWidgetData } from "@homarr/integrations"; import { firewallCpuRequestHandler, firewallInterfacesRequestHandler, @@ -10,29 +12,53 @@ import { createManyIntegrationMiddleware } from "../../middlewares/integration"; import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; -const firewallMiddleware = createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall")); +const firewallMiddleware = createManyIntegrationMiddleware( + "query", + ...getIntegrationKindsByCategory("firewall"), + "mock", +); -const queryFirewall = < - THandler extends { - handler: (integration: any, input: any) => { getDataAsync: () => Promise<{ data: any; timestamp: Date }> }; +interface FirewallResult { + integration: { id: string; name: string; kind: IntegrationKind; updatedAt: Date }; + summary: TSummary; + error?: string; +} + +const queryFirewall = ( + handler: { + handler: ( + integration: any, + input: Record, + ) => { getDataAsync: () => Promise<{ data: TSummary; timestamp: Date }> }; }, ->( - handler: THandler, - createFallback: () => Awaited["getDataAsync"]>>["data"], + createFallback: () => TSummary, + getMockData: () => TSummary, ) => publicProcedure.concat(firewallMiddleware).query(async ({ ctx }) => settleIntegrationQueries( ctx.integrations, - async (integration) => { - const { data, timestamp } = await handler.handler(integration, {}).getDataAsync(); + async (integration): Promise> => { + if (integration.kind === "mock") { + return { + integration: { + id: integration.id, + name: integration.name, + kind: integration.kind, + updatedAt: new Date(mockWidgetData.timestamp), + }, + summary: getMockData(), + error: undefined, + }; + } + const { data, timestamp } = await handler.handler({ ...integration, kind: "opnsense" }, {}).getDataAsync(); return { integration: { id: integration.id, name: integration.name, kind: integration.kind, updatedAt: timestamp }, - summary: data as Awaited["getDataAsync"]>>["data"], - error: undefined as string | undefined, + summary: data, + error: undefined, }; }, { - fallback: (integration, error) => ({ + fallback: (integration, error): FirewallResult => ({ integration: { id: integration.id, name: integration.name, kind: integration.kind, updatedAt: new Date(0) }, summary: createFallback(), error: toPublicIntegrationError(error), @@ -43,8 +69,24 @@ const queryFirewall = < ); export const firewallRouter = createTRPCRouter({ - getFirewallCpuStatus: queryFirewall(firewallCpuRequestHandler, () => ({ total: 0 })), - getFirewallInterfacesStatus: queryFirewall(firewallInterfacesRequestHandler, () => []), - getFirewallVersionStatus: queryFirewall(firewallVersionRequestHandler, () => ({ version: "Unknown" })), - getFirewallMemoryStatus: queryFirewall(firewallMemoryRequestHandler, () => ({ used: 0, total: 0, percent: 0 })), + getFirewallCpuStatus: queryFirewall( + firewallCpuRequestHandler, + () => ({ total: 0 }), + () => mockWidgetData.firewallCpu, + ), + getFirewallInterfacesStatus: queryFirewall( + firewallInterfacesRequestHandler, + () => [], + () => mockWidgetData.firewallInterfaces, + ), + getFirewallVersionStatus: queryFirewall( + firewallVersionRequestHandler, + () => ({ version: "Unknown" }), + () => mockWidgetData.firewallVersion, + ), + getFirewallMemoryStatus: queryFirewall( + firewallMemoryRequestHandler, + () => ({ used: 0, total: 0, percent: 0 }), + () => mockWidgetData.firewallMemory, + ), }); diff --git a/packages/api/src/router/widgets/immich.ts b/packages/api/src/router/widgets/immich.ts index 136d0e111..75966c793 100644 --- a/packages/api/src/router/widgets/immich.ts +++ b/packages/api/src/router/widgets/immich.ts @@ -1,6 +1,7 @@ import { z } from "zod/v4"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; +import { mockWidgetData } from "@homarr/integrations"; import { immichAlbumRequestHandler, immichAlbumsRequestHandler, @@ -12,11 +13,12 @@ import { createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createTRPCRouter, publicProcedure } from "../../trpc"; const createImmichIntegrationMiddleware = (action: IntegrationAction) => - createOneIntegrationMiddleware(action, ...getIntegrationKindsByCategory("photoService")); + createOneIntegrationMiddleware(action, ...getIntegrationKindsByCategory("photoService"), "mock"); export const immichRouter = createTRPCRouter({ getServerStats: publicProcedure.concat(createImmichIntegrationMiddleware("query")).query(async ({ ctx }) => { - const innerHandler = immichStatsRequestHandler.handler(ctx.integration, {}); + if (ctx.integration.kind === "mock") return mockWidgetData.immichStats; + const innerHandler = immichStatsRequestHandler.handler({ ...ctx.integration, kind: "immich" }, {}); const data = await innerHandler.getDataAsync(); return data.data; }), @@ -29,7 +31,13 @@ export const immichRouter = createTRPCRouter({ }), ) .query(async ({ ctx, input }) => { - const innerHandler = immichAlbumRequestHandler.handler(ctx.integration, { albumId: input.albumId }); + if (ctx.integration.kind === "mock") return mockWidgetData.immichAlbum; + const innerHandler = immichAlbumRequestHandler.handler( + { ...ctx.integration, kind: "immich" }, + { + albumId: input.albumId, + }, + ); const data = await innerHandler.getDataAsync(); return data.data; }), @@ -42,7 +50,13 @@ export const immichRouter = createTRPCRouter({ ) .concat(createImmichIntegrationMiddleware("query")) .query(async ({ ctx, input }) => { - const innerHandler = immichAlbumsRequestHandler.handler(ctx.integration, { limit: input.limit }); + if (ctx.integration.kind === "mock") return mockWidgetData.immichAlbums.slice(0, input.limit); + const innerHandler = immichAlbumsRequestHandler.handler( + { ...ctx.integration, kind: "immich" }, + { + limit: input.limit, + }, + ); const data = await innerHandler.getDataAsync(); return data.data; }), diff --git a/packages/api/src/router/widgets/paperless-ngx.ts b/packages/api/src/router/widgets/paperless-ngx.ts index 2a1bf40de..0adcb2fc5 100644 --- a/packages/api/src/router/widgets/paperless-ngx.ts +++ b/packages/api/src/router/widgets/paperless-ngx.ts @@ -1,12 +1,16 @@ import { paperlessNgxStatsRequestHandler } from "@homarr/request-handler/paperless-ngx"; +import { mockWidgetData } from "@homarr/integrations"; import { createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createTRPCRouter, publicProcedure } from "../../trpc"; export const paperlessNgxRouter = createTRPCRouter({ - getStats: publicProcedure.concat(createOneIntegrationMiddleware("query", "paperlessNgx")).query(async ({ ctx }) => { - const innerHandler = paperlessNgxStatsRequestHandler.handler(ctx.integration, {}); - const data = await innerHandler.getDataAsync(); - return data.data; - }), + getStats: publicProcedure + .concat(createOneIntegrationMiddleware("query", "paperlessNgx", "mock")) + .query(async ({ ctx }) => { + if (ctx.integration.kind === "mock") return mockWidgetData.paperlessNgx; + const innerHandler = paperlessNgxStatsRequestHandler.handler({ ...ctx.integration, kind: "paperlessNgx" }, {}); + const data = await innerHandler.getDataAsync(); + return data.data; + }), }); diff --git a/packages/api/src/router/widgets/patchmon.ts b/packages/api/src/router/widgets/patchmon.ts index aee758823..2ea98bb10 100644 --- a/packages/api/src/router/widgets/patchmon.ts +++ b/packages/api/src/router/widgets/patchmon.ts @@ -1,4 +1,5 @@ import { patchmonStatsRequestHandler } from "@homarr/request-handler/patchmon"; +import { mockWidgetData } from "@homarr/integrations"; import { createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createTRPCRouter, protectedProcedure } from "../../trpc"; @@ -12,9 +13,10 @@ export const patchmonRouter = createTRPCRouter({ "Get PatchMon host patch statistics including total hosts, hosts needing updates, security update counts, up-to-date hosts, outdated packages, repositories, and OS distribution. REQUIRED: integrationId (single PatchMon integration ID from integration_all). Requires authenticated session (API key or browser login) and integration query access.", }, }) - .concat(createOneIntegrationMiddleware("query", "patchmon")) + .concat(createOneIntegrationMiddleware("query", "patchmon", "mock")) .query(async ({ ctx }) => { - const innerHandler = patchmonStatsRequestHandler.handler(ctx.integration, {}); + if (ctx.integration.kind === "mock") return mockWidgetData.patchmon; + const innerHandler = patchmonStatsRequestHandler.handler({ ...ctx.integration, kind: "patchmon" }, {}); const { data } = await innerHandler.getDataAsync(); return data; }), diff --git a/packages/api/src/router/widgets/speedtest-tracker.ts b/packages/api/src/router/widgets/speedtest-tracker.ts index 4730940a4..7770ed051 100644 --- a/packages/api/src/router/widgets/speedtest-tracker.ts +++ b/packages/api/src/router/widgets/speedtest-tracker.ts @@ -1,4 +1,5 @@ import { speedtestTrackerRequestHandler } from "@homarr/request-handler/speedtest-tracker"; +import { mockWidgetData } from "@homarr/integrations"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations"; @@ -6,12 +7,22 @@ import { createTRPCRouter, publicProcedure } from "../../trpc"; export const speedtestTrackerRouter = createTRPCRouter({ getDashboard: publicProcedure - .concat(createManyIntegrationMiddleware("query", "speedtestTracker")) + .concat(createManyIntegrationMiddleware("query", "speedtestTracker", "mock")) .query(async ({ ctx }) => { return await settleIntegrationQueries( ctx.integrations, async (integration) => { - const innerHandler = speedtestTrackerRequestHandler.handler(integration, {}); + if (integration.kind === "mock") { + return { + integrationId: integration.id, + integrationName: integration.name, + integrationUrl: integration.url, + dashboard: mockWidgetData.speedtestTracker, + updatedAt: new Date(mockWidgetData.timestamp), + error: undefined, + }; + } + const innerHandler = speedtestTrackerRequestHandler.handler({ ...integration, kind: "speedtestTracker" }, {}); const { data, timestamp } = await innerHandler.getDataAsync(); return { diff --git a/packages/api/src/router/widgets/tracearr.ts b/packages/api/src/router/widgets/tracearr.ts index d96a02932..f8960d1cf 100644 --- a/packages/api/src/router/widgets/tracearr.ts +++ b/packages/api/src/router/widgets/tracearr.ts @@ -1,37 +1,50 @@ import { tracearrRequestHandler } from "@homarr/request-handler/tracearr"; +import { mockWidgetData } from "@homarr/integrations"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; export const tracearrRouter = createTRPCRouter({ - getDashboard: publicProcedure.concat(createManyIntegrationMiddleware("query", "tracearr")).query(async ({ ctx }) => { - return await settleIntegrationQueries( - ctx.integrations, - async (integration) => { - const innerHandler = tracearrRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getDataAsync(); + getDashboard: publicProcedure + .concat(createManyIntegrationMiddleware("query", "tracearr", "mock")) + .query(async ({ ctx }) => { + return await settleIntegrationQueries( + ctx.integrations, + async (integration) => { + if (integration.kind === "mock") { + return { + integrationId: integration.id, + integrationName: integration.name, + integrationUrl: integration.url, + dashboard: mockWidgetData.tracearr, + updatedAt: new Date(mockWidgetData.timestamp), + error: undefined, + }; + } + const innerHandler = tracearrRequestHandler.handler({ ...integration, kind: "tracearr" }, {}); + const { data, timestamp } = await innerHandler.getDataAsync(); - return { - integrationId: integration.id, - integrationName: integration.name, - integrationUrl: integration.url, - dashboard: data as typeof data | null, - updatedAt: timestamp, - error: undefined as string | undefined, - }; - }, - { - fallback: (integration, error) => ({ - integrationId: integration.id, - integrationName: integration.name, - integrationUrl: integration.url, - dashboard: null, - updatedAt: new Date(0), - error: toPublicIntegrationError(error), - }), - throwOnAllFailures: true, - }, - ); - }), + return { + integrationId: integration.id, + integrationName: integration.name, + integrationUrl: integration.url, + dashboard: data as typeof data | null, + updatedAt: timestamp, + error: undefined as string | undefined, + }; + }, + { + fallback: (integration, error) => ({ + integrationId: integration.id, + integrationName: integration.name, + integrationUrl: integration.url, + dashboard: null, + updatedAt: new Date(0), + error: toPublicIntegrationError(error), + }), + throwOnAllFailures: true, + }, + ); + }), }); diff --git a/packages/api/src/router/widgets/traefik.ts b/packages/api/src/router/widgets/traefik.ts index ad7e8479d..a80891fd7 100644 --- a/packages/api/src/router/widgets/traefik.ts +++ b/packages/api/src/router/widgets/traefik.ts @@ -1,4 +1,5 @@ import { traefikRequestHandler } from "@homarr/request-handler/traefik"; +import { mockWidgetData } from "@homarr/integrations"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations"; @@ -13,12 +14,22 @@ export const traefikRouter = createTRPCRouter({ "Retrieve Traefik dashboard data, including routers, services, middlewares, and entry points, for the given integration IDs.", }, }) - .concat(createManyIntegrationMiddleware("query", "traefik")) + .concat(createManyIntegrationMiddleware("query", "traefik", "mock")) .query(async ({ ctx }) => { return await settleIntegrationQueries( ctx.integrations, async (integration) => { - const innerHandler = traefikRequestHandler.handler(integration, {}); + if (integration.kind === "mock") { + return { + integrationId: integration.id, + integrationName: integration.name, + integrationUrl: integration.url, + dashboard: mockWidgetData.traefik, + updatedAt: new Date(mockWidgetData.timestamp), + error: undefined, + }; + } + const innerHandler = traefikRequestHandler.handler({ ...integration, kind: "traefik" }, {}); const { data, timestamp } = await innerHandler.getDataAsync(); return { diff --git a/packages/api/src/router/widgets/umami.ts b/packages/api/src/router/widgets/umami.ts index 3994f398e..c5736be53 100644 --- a/packages/api/src/router/widgets/umami.ts +++ b/packages/api/src/router/widgets/umami.ts @@ -2,7 +2,7 @@ import { z } from "zod/v4"; import { createLogger } from "@homarr/core/infrastructure/logs"; import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; -import { createIntegrationAsync } from "@homarr/integrations"; +import { createIntegrationAsync, mockWidgetData } from "@homarr/integrations"; import { umamiActiveVisitorsRequestHandler, umamiEventNamesRequestHandler, @@ -19,15 +19,18 @@ import { createTRPCRouter, publicProcedure } from "../../trpc"; const logger = createLogger({ module: "umami-router" }); export const umamiRouter = createTRPCRouter({ - getWebsites: publicProcedure.concat(createOneIntegrationMiddleware("query", "umami")).query(async ({ ctx }) => { - try { - const instance = await createIntegrationAsync(ctx.integration); - return await instance.getWebsitesAsync(); - } catch (error) { - logger.warn(new Error("Failed to load websites", { cause: error })); - return []; - } - }), + getWebsites: publicProcedure + .concat(createOneIntegrationMiddleware("query", "umami", "mock")) + .query(async ({ ctx }) => { + if (ctx.integration.kind === "mock") return mockWidgetData.umamiWebsites; + try { + const instance = await createIntegrationAsync({ ...ctx.integration, kind: "umami" }); + return await instance.getWebsitesAsync(); + } catch (error) { + logger.warn(new Error("Failed to load websites", { cause: error })); + return []; + } + }), getVisitorStats: publicProcedure .input( @@ -37,14 +40,26 @@ export const umamiRouter = createTRPCRouter({ eventName: z.string().optional(), }), ) - .concat(createManyIntegrationMiddleware("query", "umami")) + .concat(createManyIntegrationMiddleware("query", "umami", "mock")) .query(async ({ ctx, input }) => { return await settleIntegrationQueries(ctx.integrations, async (integration) => { - const innerHandler = umamiRequestHandler.handler(integration, { - websiteId: input.websiteId, - timeFrame: input.timeFrame, - eventName: input.eventName, - }); + if (integration.kind === "mock") { + return { + integrationId: integration.id, + integrationName: integration.name, + integrationUrl: integration.url, + visitorStats: { ...mockWidgetData.umamiVisitorStats, timeFrame: input.timeFrame }, + updatedAt: new Date(mockWidgetData.timestamp), + }; + } + const innerHandler = umamiRequestHandler.handler( + { ...integration, kind: "umami" }, + { + websiteId: input.websiteId, + timeFrame: input.timeFrame, + eventName: input.eventName, + }, + ); const { data, timestamp } = await innerHandler.getDataAsync(); return { @@ -59,10 +74,16 @@ export const umamiRouter = createTRPCRouter({ getEventNames: publicProcedure .input(z.object({ websiteId: z.string() })) - .concat(createOneIntegrationMiddleware("query", "umami")) + .concat(createOneIntegrationMiddleware("query", "umami", "mock")) .query(async ({ ctx, input }) => { + if (ctx.integration.kind === "mock") return mockWidgetData.umamiEventNames; try { - const innerHandler = umamiEventNamesRequestHandler.handler(ctx.integration, { websiteId: input.websiteId }); + const innerHandler = umamiEventNamesRequestHandler.handler( + { ...ctx.integration, kind: "umami" }, + { + websiteId: input.websiteId, + }, + ); const { data } = await innerHandler.getDataAsync(); return data; } catch (error) { @@ -81,14 +102,18 @@ export const umamiRouter = createTRPCRouter({ limit: z.number().int().min(1).max(500), }), ) - .concat(createOneIntegrationMiddleware("query", "umami")) + .concat(createOneIntegrationMiddleware("query", "umami", "mock")) .query(async ({ ctx, input }) => { + if (ctx.integration.kind === "mock") return mockWidgetData.umamiTopPages.slice(0, input.limit); try { - const innerHandler = umamiTopPagesRequestHandler.handler(ctx.integration, { - websiteId: input.websiteId, - timeFrame: input.timeFrame, - limit: input.limit, - }); + const innerHandler = umamiTopPagesRequestHandler.handler( + { ...ctx.integration, kind: "umami" }, + { + websiteId: input.websiteId, + timeFrame: input.timeFrame, + limit: input.limit, + }, + ); const { data } = await innerHandler.getDataAsync(); return data; } catch (error) { @@ -111,14 +136,18 @@ export const umamiRouter = createTRPCRouter({ limit: z.number().int().min(1).max(500), }), ) - .concat(createOneIntegrationMiddleware("query", "umami")) + .concat(createOneIntegrationMiddleware("query", "umami", "mock")) .query(async ({ ctx, input }) => { + if (ctx.integration.kind === "mock") return mockWidgetData.umamiTopReferrers.slice(0, input.limit); try { - const innerHandler = umamiTopReferrersRequestHandler.handler(ctx.integration, { - websiteId: input.websiteId, - timeFrame: input.timeFrame, - limit: input.limit, - }); + const innerHandler = umamiTopReferrersRequestHandler.handler( + { ...ctx.integration, kind: "umami" }, + { + websiteId: input.websiteId, + timeFrame: input.timeFrame, + limit: input.limit, + }, + ); const { data } = await innerHandler.getDataAsync(); return data; } catch (error) { @@ -141,15 +170,21 @@ export const umamiRouter = createTRPCRouter({ eventNames: z.array(z.string()), }), ) - .concat(createOneIntegrationMiddleware("query", "umami")) + .concat(createOneIntegrationMiddleware("query", "umami", "mock")) .query(async ({ ctx, input }) => { + if (ctx.integration.kind === "mock") { + return mockWidgetData.umamiEventSeries.filter((series) => input.eventNames.includes(series.eventName)); + } try { const sortedNames = [...input.eventNames].toSorted(); - const innerHandler = umamiMultiEventRequestHandler.handler(ctx.integration, { - websiteId: input.websiteId, - timeFrame: input.timeFrame, - eventNames: sortedNames, - }); + const innerHandler = umamiMultiEventRequestHandler.handler( + { ...ctx.integration, kind: "umami" }, + { + websiteId: input.websiteId, + timeFrame: input.timeFrame, + eventNames: sortedNames, + }, + ); const { data } = await innerHandler.getDataAsync(); return data; } catch (error) { @@ -166,12 +201,16 @@ export const umamiRouter = createTRPCRouter({ getActiveVisitors: publicProcedure .input(z.object({ websiteId: z.string() })) - .concat(createOneIntegrationMiddleware("query", "umami")) + .concat(createOneIntegrationMiddleware("query", "umami", "mock")) .query(async ({ ctx, input }) => { + if (ctx.integration.kind === "mock") return 7; try { - const innerHandler = umamiActiveVisitorsRequestHandler.handler(ctx.integration, { - websiteId: input.websiteId, - }); + const innerHandler = umamiActiveVisitorsRequestHandler.handler( + { ...ctx.integration, kind: "umami" }, + { + websiteId: input.websiteId, + }, + ); const { data } = await innerHandler.getDataAsync(); return data; } catch (error) { diff --git a/packages/api/src/router/widgets/uptime-kuma.ts b/packages/api/src/router/widgets/uptime-kuma.ts index e3082c284..68104158d 100644 --- a/packages/api/src/router/widgets/uptime-kuma.ts +++ b/packages/api/src/router/widgets/uptime-kuma.ts @@ -1,4 +1,5 @@ import { uptimeKumaRequestHandler } from "@homarr/request-handler/uptime-kuma"; +import { mockWidgetData } from "@homarr/integrations"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations"; @@ -6,12 +7,22 @@ import { createTRPCRouter, publicProcedure } from "../../trpc"; export const uptimeKumaRouter = createTRPCRouter({ getDashboard: publicProcedure - .concat(createManyIntegrationMiddleware("query", "uptimeKuma")) + .concat(createManyIntegrationMiddleware("query", "uptimeKuma", "mock")) .query(async ({ ctx }) => { return await settleIntegrationQueries( ctx.integrations, async (integration) => { - const innerHandler = uptimeKumaRequestHandler.handler(integration, {}); + if (integration.kind === "mock") { + return { + integrationId: integration.id, + integrationName: integration.name, + integrationUrl: integration.url, + dashboard: mockWidgetData.uptimeKuma, + updatedAt: new Date(mockWidgetData.timestamp), + error: undefined, + }; + } + const innerHandler = uptimeKumaRequestHandler.handler({ ...integration, kind: "uptimeKuma" }, {}); const { data, timestamp } = await innerHandler.getDataAsync(); return { diff --git a/packages/api/src/router/widgets/vpn.ts b/packages/api/src/router/widgets/vpn.ts index 276367440..0d1ff146e 100644 --- a/packages/api/src/router/widgets/vpn.ts +++ b/packages/api/src/router/widgets/vpn.ts @@ -1,18 +1,41 @@ import { getIntegrationKindsByCategory } from "@homarr/definitions"; +import type { IntegrationKind } from "@homarr/definitions"; +import { mockWidgetData } from "@homarr/integrations"; +import type { VpnSummary } from "@homarr/integrations/types"; import { vpnSummaryHandler } from "@homarr/request-handler/vpn"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; +interface VpnResult { + integration: { id: string; name: string; kind: IntegrationKind; updatedAt: Date }; + summary: VpnSummary | null; + error?: string; +} + export const vpnRouter = createTRPCRouter({ getSummaries: publicProcedure - .unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("vpn"))) + .unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("vpn"), "mock")) .query(async ({ ctx }) => { return await settleIntegrationQueries( ctx.integrations, - async (integration) => { - const { data, timestamp } = await vpnSummaryHandler.handler(integration, {}).getDataAsync(); + async (integration): Promise => { + if (integration.kind === "mock") { + return { + integration: { + id: integration.id, + name: integration.name, + kind: integration.kind, + updatedAt: new Date(mockWidgetData.timestamp), + }, + summary: mockWidgetData.vpn, + error: undefined, + }; + } + const { data, timestamp } = await vpnSummaryHandler + .handler({ ...integration, kind: "gluetun" }, {}) + .getDataAsync(); return { integration: { id: integration.id, @@ -25,7 +48,7 @@ export const vpnRouter = createTRPCRouter({ }; }, { - fallback: (integration, error) => ({ + fallback: (integration, error): VpnResult => ({ integration: { id: integration.id, name: integration.name, diff --git a/packages/api/src/router/widgets/wud.ts b/packages/api/src/router/widgets/wud.ts index 4b510e660..bb7ed3f3f 100644 --- a/packages/api/src/router/widgets/wud.ts +++ b/packages/api/src/router/widgets/wud.ts @@ -1,4 +1,5 @@ import { wudStatsRequestHandler } from "@homarr/request-handler/wud"; +import { mockWidgetData } from "@homarr/integrations"; import { createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createTRPCRouter, publicProcedure } from "../../trpc"; @@ -9,12 +10,15 @@ export const wudRouter = createTRPCRouter({ mcp: { enabled: true, description: - "Returns monitored-container counts and available updates from a What's Up Docker integration. REQUIRED: integrationId from integration_all. The caller needs query permission for that integration.", + "Returns monitored-container counts and available updates from a What's Up Docker integration, or mock statistics from a mock integration. REQUIRED: integrationId from integration_all. The caller needs query permission for that integration.", }, }) - .concat(createOneIntegrationMiddleware("query", "wud")) + .concat(createOneIntegrationMiddleware("query", "wud", "mock")) .query(async ({ ctx }) => { - const handler = wudStatsRequestHandler.handler(ctx.integration, {}); + if (ctx.integration.kind === "mock") { + return { stats: mockWidgetData.wud, updatedAt: new Date(mockWidgetData.timestamp) }; + } + const handler = wudStatsRequestHandler.handler({ ...ctx.integration, kind: "wud" }, {}); const { data, timestamp } = await handler.getDataAsync(); return { diff --git a/packages/db/migrations/seed.ts b/packages/db/migrations/seed.ts index f47bac705..3e4cdebf2 100644 --- a/packages/db/migrations/seed.ts +++ b/packages/db/migrations/seed.ts @@ -730,6 +730,168 @@ const buildDemoWidgets = (appIds: string[], customWidgetDefinitionId: string): D { kind: "notifications", xOffset: 3, yOffset: 9, width: 2, height: 2, needsIntegration: true }, { kind: "beszelAlerts", xOffset: 5, yOffset: 9, width: 4, height: 2, needsIntegration: true }, + // Extended integrations + { kind: "mediaTranscoding", xOffset: 0, yOffset: 14, width: 4, height: 3, needsIntegration: true }, + { kind: "beszelSystemTable", xOffset: 4, yOffset: 14, width: 4, height: 3, needsIntegration: true }, + { kind: "networkControllerSummary", xOffset: 8, yOffset: 14, width: 4, height: 3, needsIntegration: true }, + { + kind: "networkControllerStatus", + xOffset: 0, + yOffset: 17, + width: 3, + height: 2, + needsIntegration: true, + options: { content: "wifi" }, + }, + { kind: "ups", xOffset: 3, yOffset: 17, width: 3, height: 2, needsIntegration: true }, + { + kind: "smartHome-entityState", + xOffset: 6, + yOffset: 17, + width: 3, + height: 2, + needsIntegration: true, + options: { entityId: "sensor.homarr_demo", displayName: "Demo environment", entityUnit: "online" }, + }, + { + kind: "smartHome-executeAutomation", + xOffset: 9, + yOffset: 17, + width: 3, + height: 2, + needsIntegration: true, + options: { automationId: "automation.homarr_demo", displayName: "Run demo routine" }, + }, + + // Everyday tools + { kind: "dnsHoleControls", xOffset: 0, yOffset: 19, width: 2, height: 2, needsIntegration: true }, + { + kind: "bookmarks", + xOffset: 2, + yOffset: 19, + width: 4, + height: 2, + needsIntegration: false, + options: { title: "Launchpad", layout: "grid", items: appIds.slice(0, 6), openNewTab: true }, + }, + { + kind: "countdown", + xOffset: 6, + yOffset: 19, + width: 2, + height: 2, + needsIntegration: false, + options: { + events: [ + { + id: "demo-new-year", + label: "New year 2030", + targetUtc: "2030-01-01T00:00:00.000Z", + startUtc: "2026-01-01T00:00:00.000Z", + timeZone: "Europe/Paris", + recurrence: "none", + }, + ], + showProgress: true, + showSeconds: false, + }, + }, + { kind: "stockPrice", xOffset: 8, yOffset: 19, width: 2, height: 2, needsIntegration: false }, + { kind: "releases", xOffset: 10, yOffset: 19, width: 2, height: 2, needsIntegration: false }, + { + kind: "iframe", + xOffset: 0, + yOffset: 21, + width: 4, + height: 3, + needsIntegration: false, + options: { + embedUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/homarr.svg", + allowScrolling: false, + }, + }, + { + kind: "video", + xOffset: 4, + yOffset: 21, + width: 4, + height: 3, + needsIntegration: false, + options: { + feedUrl: "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4", + hasAutoPlay: false, + isMuted: true, + hasControls: true, + }, + }, + { kind: "minecraftServerStatus", xOffset: 8, yOffset: 21, width: 2, height: 3, needsIntegration: false }, + { + kind: "timetable", + xOffset: 10, + yOffset: 21, + width: 2, + height: 3, + needsIntegration: false, + options: { baseUrl: "https://search.ch", station: { value: "8507000", label: "Bern" } }, + }, + + // Complete mock integration gallery + { + kind: "immich-albumCarousel", + xOffset: 0, + yOffset: 24, + width: 3, + height: 3, + needsIntegration: true, + options: { albumId: "demo-paris", rotationIntervalSeconds: 8, showPhotoInfo: true, randomizePhotos: false }, + }, + { kind: "coolify", xOffset: 3, yOffset: 24, width: 3, height: 3, needsIntegration: true }, + { kind: "systemResources", xOffset: 6, yOffset: 24, width: 3, height: 3, needsIntegration: true }, + { kind: "systemDisks", xOffset: 9, yOffset: 24, width: 3, height: 3, needsIntegration: true }, + + { kind: "immich-serverStats", xOffset: 0, yOffset: 27, width: 2, height: 3, needsIntegration: true }, + { kind: "paperlessNgx", xOffset: 2, yOffset: 27, width: 2, height: 3, needsIntegration: true }, + { kind: "patchmon", xOffset: 4, yOffset: 27, width: 2, height: 3, needsIntegration: true }, + { + kind: "speedtestTracker", + xOffset: 6, + yOffset: 27, + width: 4, + height: 3, + needsIntegration: true, + options: { showLatestResult: true, showStats: true, showRecentResults: false, showPingGraph: false }, + }, + { kind: "audioStats", xOffset: 10, yOffset: 27, width: 2, height: 3, needsIntegration: true }, + + { kind: "uptimeKuma", xOffset: 0, yOffset: 30, width: 3, height: 2, needsIntegration: true }, + { kind: "firewall", xOffset: 3, yOffset: 30, width: 3, height: 2, needsIntegration: true }, + { kind: "vpn", xOffset: 6, yOffset: 30, width: 3, height: 2, needsIntegration: true }, + { kind: "bazarr", xOffset: 9, yOffset: 30, width: 3, height: 2, needsIntegration: true }, + + { kind: "archiveTeamWarrior", xOffset: 0, yOffset: 32, width: 3, height: 2, needsIntegration: true }, + { kind: "wud", xOffset: 3, yOffset: 32, width: 3, height: 2, needsIntegration: true }, + { + kind: "anchorNote", + xOffset: 6, + yOffset: 32, + width: 2, + height: 2, + needsIntegration: true, + options: { noteId: "homarr-demo-note", showTitle: true, showUpdatedAt: true }, + }, + { kind: "tracearr", xOffset: 8, yOffset: 32, width: 4, height: 2, needsIntegration: true }, + + { kind: "traefik", xOffset: 0, yOffset: 34, width: 6, height: 2, needsIntegration: true }, + { + kind: "umami", + xOffset: 6, + yOffset: 34, + width: 6, + height: 2, + needsIntegration: true, + options: { websiteId: "homarr-demo", timeFrame: "24h", viewMode: "chart", chartType: "bar" }, + }, + // Right app rail ...appIds.map( (appId, index): DemoWidget => ({ diff --git a/packages/db/test/sqlite-migration.spec.ts b/packages/db/test/sqlite-migration.spec.ts index 85b6a427b..4026d7f3a 100644 --- a/packages/db/test/sqlite-migration.spec.ts +++ b/packages/db/test/sqlite-migration.spec.ts @@ -7,6 +7,7 @@ import SuperJSON from "superjson"; import { expect, test } from "vitest"; import { DB_CASING } from "@homarr/core/infrastructure/db/constants"; +import { widgetIntegrationSupport, widgetKinds } from "@homarr/definitions"; import type { Database } from ".."; import { seedDataAsync } from "../migrations/seed"; @@ -45,7 +46,7 @@ test("SQLite migrations seed the five disabled bundled custom widgets", async () connection.close(); }); -test("SQLite migrations seed the redesigned demo dashboard", async () => { +test("SQLite migrations seed the complete demo widget gallery", async () => { const previousDemoMode = process.env.DEMO_MODE; process.env.DEMO_MODE = "true"; const connection = new BetterSqlite3(":memory:"); @@ -65,7 +66,7 @@ test("SQLite migrations seed the redesigned demo dashboard", async () => { with: { layouts: true, sections: { with: { collapseStates: true, layouts: true } }, - items: { with: { layouts: true } }, + items: { with: { integrations: true, layouts: true } }, }, }); if (!board) throw new Error("Demo board was not seeded"); @@ -126,6 +127,18 @@ test("SQLite migrations seed the redesigned demo dashboard", async () => { expectItemLayout("notifications", 3, 9, 2, 2); expectItemLayout("beszelAlerts", 5, 9, 4, 2); + expect(new Set(board.items.map((item) => item.kind))).toEqual(new Set(widgetKinds)); + const mockIntegration = await database.query.integrations.findFirst({ + where: (table, { eq }) => eq(table.kind, "mock"), + }); + if (!mockIntegration) throw new Error("Demo mock integration was not seeded"); + for (const item of board.items) { + if (widgetIntegrationSupport[item.kind] === undefined) continue; + expect(item.integrations).toEqual([ + expect.objectContaining({ integrationId: mockIntegration.id, itemId: item.id }), + ]); + } + for (const kind of ["weather", "airQuality"]) { const item = itemByKind(kind); if (!item?.options) throw new Error(`Demo ${kind} options were not seeded`); diff --git a/packages/definitions/src/test/widget-integration-map.spec.ts b/packages/definitions/src/test/widget-integration-map.spec.ts index 33407069e..9e21bbdcd 100644 --- a/packages/definitions/src/test/widget-integration-map.spec.ts +++ b/packages/definitions/src/test/widget-integration-map.spec.ts @@ -26,6 +26,12 @@ describe("native feature capability descriptor", () => { } }); + test("supports the mock integration for every integration-backed widget", () => { + for (const capability of Object.values(nativeFeatureCapabilities)) { + expect(capability.integrations).toContain("mock"); + } + }); + test("derives optional connections and server selection limits", () => { expect(widgetKindsWithOptionalIntegrations).toEqual(new Set(["calendar"])); expect(widgetIntegrationLimits).toEqual({ diff --git a/packages/definitions/src/widget-integration-map.ts b/packages/definitions/src/widget-integration-map.ts index b83fa3706..b9fbfb929 100644 --- a/packages/definitions/src/widget-integration-map.ts +++ b/packages/definitions/src/widget-integration-map.ts @@ -41,32 +41,32 @@ export const nativeFeatureCapabilities = { (kind) => kind !== "patchmon" && kind !== "wud", ), }, - firewall: { integrations: getIntegrationKindsByCategory("firewall") }, + firewall: { integrations: [...getIntegrationKindsByCategory("firewall"), "mock"] }, notifications: { integrations: getIntegrationKindsByCategory("notifications") }, mediaReleases: { integrations: ["mock", "emby", "jellyfin", "plex"] }, - systemResources: { integrations: ["dashDot", "openmediavault", "truenas", "unraid", "glances", "synology"] }, - systemDisks: { integrations: ["dashDot", "openmediavault", "truenas", "unraid", "synology"] }, + systemResources: { integrations: ["dashDot", "openmediavault", "truenas", "unraid", "glances", "synology", "mock"] }, + systemDisks: { integrations: ["dashDot", "openmediavault", "truenas", "unraid", "synology", "mock"] }, beszelAlerts: { integrations: ["beszel", "mock"] }, beszelSystemGrid: { integrations: ["beszel", "mock"] }, beszelSystemStats: { integrations: ["beszel", "mock"] }, beszelSystemTable: { integrations: ["beszel", "mock"] }, - coolify: { integrations: ["coolify"] }, - "immich-serverStats": { integrations: ["immich"], serverMaxIntegrations: 1 }, - "immich-albumCarousel": { integrations: ["immich"], serverMaxIntegrations: 1 }, - paperlessNgx: { integrations: ["paperlessNgx"], serverMaxIntegrations: 1 }, - patchmon: { integrations: ["patchmon"], serverMaxIntegrations: 1 }, - bazarr: { integrations: ["bazarr"], serverMaxIntegrations: 1 }, - tracearr: { integrations: ["tracearr"] }, - speedtestTracker: { integrations: ["speedtestTracker"] }, - uptimeKuma: { integrations: ["uptimeKuma"] }, - audioStats: { integrations: ["navidrome", "audiobookshelf"], serverMaxIntegrations: 1 }, - umami: { integrations: ["umami"], serverMaxIntegrations: 1 }, - vpn: { integrations: getIntegrationKindsByCategory("vpn") }, + coolify: { integrations: ["coolify", "mock"] }, + "immich-serverStats": { integrations: ["immich", "mock"], serverMaxIntegrations: 1 }, + "immich-albumCarousel": { integrations: ["immich", "mock"], serverMaxIntegrations: 1 }, + paperlessNgx: { integrations: ["paperlessNgx", "mock"], serverMaxIntegrations: 1 }, + patchmon: { integrations: ["patchmon", "mock"], serverMaxIntegrations: 1 }, + bazarr: { integrations: ["bazarr", "mock"], serverMaxIntegrations: 1 }, + tracearr: { integrations: ["tracearr", "mock"] }, + speedtestTracker: { integrations: ["speedtestTracker", "mock"] }, + uptimeKuma: { integrations: ["uptimeKuma", "mock"] }, + audioStats: { integrations: ["navidrome", "audiobookshelf", "mock"], serverMaxIntegrations: 1 }, + umami: { integrations: ["umami", "mock"], serverMaxIntegrations: 1 }, + vpn: { integrations: [...getIntegrationKindsByCategory("vpn"), "mock"] }, ups: { integrations: getIntegrationKindsByCategory("ups") }, - archiveTeamWarrior: { integrations: ["archiveTeamWarrior"], serverMaxIntegrations: 1 }, - anchorNote: { integrations: ["anchor"], serverMaxIntegrations: 1 }, - traefik: { integrations: ["traefik"] }, - wud: { integrations: ["wud"], serverMaxIntegrations: 1 }, + archiveTeamWarrior: { integrations: ["archiveTeamWarrior", "mock"], serverMaxIntegrations: 1 }, + anchorNote: { integrations: ["anchor", "mock"], serverMaxIntegrations: 1 }, + traefik: { integrations: ["traefik", "mock"] }, + wud: { integrations: ["wud", "mock"], serverMaxIntegrations: 1 }, } as const satisfies Partial>; const nativeFeatureCapabilityEntries = Object.entries(nativeFeatureCapabilities) as [ diff --git a/packages/integrations/src/index.ts b/packages/integrations/src/index.ts index d4f200618..05d3a7764 100644 --- a/packages/integrations/src/index.ts +++ b/packages/integrations/src/index.ts @@ -45,6 +45,7 @@ export { UmamiIntegration } from "./umami/umami-integration"; export { PeaNutIntegration } from "./peanut/peanut-integration"; export { BazarrIntegration } from "./bazarr/bazarr-integration"; export { TraefikIntegration } from "./traefik/traefik-integration"; +export { mockWidgetData } from "./mock/widget-data"; // Types export type { IntegrationInput } from "./base/integration"; diff --git a/packages/integrations/src/mock/widget-data.ts b/packages/integrations/src/mock/widget-data.ts new file mode 100644 index 000000000..5df6f8dcd --- /dev/null +++ b/packages/integrations/src/mock/widget-data.ts @@ -0,0 +1,355 @@ +import type { AnchorNote, AnchorNoteSummary } from "../anchor/anchor-types"; +import type { ArchiveTeamWarriorStatus } from "../archive-team-warrior/archive-team-warrior-types"; +import type { BazarrBadges } from "../bazarr/bazarr-types"; +import type { CoolifyInstanceInfo } from "../coolify/coolify-types"; +import type { ImmichAlbum, ImmichServerStats } from "../immich/immich-integration"; +import type { NavidromeDashboardData } from "../navidrome/navidrome-types"; +import type { PaperlessNgxStats } from "../paperless-ngx/paperless-ngx-types"; +import type { PatchMonStats } from "../patchmon/patchmon-types"; +import type { SpeedtestTrackerDashboardData } from "../speedtest-tracker/speedtest-tracker-types"; +import type { TracearrDashboardData } from "../tracearr/tracearr-types"; +import type { TraefikDashboardData } from "../traefik/traefik-types"; +import type { UmamiEventSeries, UmamiMetricItem, UmamiVisitorStats, UmamiWebsite } from "../umami/umami-types"; +import type { UptimeKumaDashboardData } from "../uptime-kuma/uptime-kuma-types"; +import type { WudStats } from "../wud/wud-types"; +import type { + FirewallCpuSummary, + FirewallInterfacesSummary, + FirewallMemorySummary, + FirewallVersionSummary, +} from "../interfaces/firewall-summary/firewall-summary-types"; +import type { VpnSummary } from "../interfaces/vpn-summary/vpn-summary-types"; + +const demoTimestamp = "2026-08-24T12:00:00.000Z"; + +const anchorNote: AnchorNote = { + id: "homarr-demo-note", + title: "Homarr demo runbook", + content: JSON.stringify({ + ops: [ + { insert: "Welcome to the complete Homarr widget gallery.\n", attributes: { bold: true } }, + { insert: "Every integration-backed card below is powered by safe mock data.\n" }, + ], + }), + createdAt: demoTimestamp, + updatedAt: demoTimestamp, + isPinned: true, + isArchived: false, + tagIds: ["demo"], + permission: "editor", + background: null, + userId: "demo", +}; + +export const mockWidgetData = { + timestamp: demoTimestamp, + anchorNote, + anchorNotes: [ + { + id: anchorNote.id, + title: anchorNote.title, + updatedAt: anchorNote.updatedAt, + isPinned: anchorNote.isPinned, + tagIds: anchorNote.tagIds, + permission: anchorNote.permission, + }, + ] satisfies AnchorNoteSummary[], + archiveTeamWarrior: { + status: "running", + runnerStatus: "running", + project: { id: "urlteam2", title: "URLTeam 2" }, + selectedProject: "urlteam2", + broadcastMessage: "Archiving the open web, one URL at a time.", + bandwidth: { + received: 8_942_000_000, + sent: 1_284_000_000, + receiving: 4_800_000, + sending: 720_000, + session_id: "homarr-demo-session", + }, + items: [ + { id: "demo-1", name: "urls-20260824-01", status: "running", project: "urlteam2", startTime: 1_777_000_000 }, + { id: "demo-2", name: "urls-20260824-00", status: "completed", project: "urlteam2" }, + ], + counts: { running: 1, completed: 284, failed: 2, canceled: 0 }, + updatedAt: demoTimestamp, + } satisfies ArchiveTeamWarriorStatus, + audioStats: { artistCount: 842, albumCount: 1_976, songCount: 24_681 } satisfies NavidromeDashboardData, + bazarr: { + episodes: 12, + movies: 4, + providers: 6, + status: 0, + sonarr_signalr: "connected", + radarr_signalr: "connected", + announcements: 1, + } satisfies BazarrBadges, + coolify: { + version: "4.0.0-beta.420", + servers: [ + { + id: 1, + uuid: "homarr-demo-edge", + name: "Paris edge", + ip: "10.0.0.20", + is_reachable: true, + is_usable: true, + settings: { server_id: 1, is_build_server: true, is_reachable: true, is_usable: true }, + }, + ], + applications: [ + { + id: 1, + uuid: "homarr-demo-app", + name: "Homarr", + status: "running:healthy", + fqdn: "https://homarr.dev", + projectName: "Home cloud", + environmentName: "Production", + }, + { + id: 2, + uuid: "media-demo-app", + name: "Media stack", + status: "running:healthy", + fqdn: null, + projectName: "Home cloud", + environmentName: "Production", + }, + ], + services: [ + { + id: 3, + uuid: "postgres-demo-service", + name: "Postgres", + status: "running:healthy", + fqdn: null, + applications: [], + projectName: "Home cloud", + environmentName: "Production", + }, + ], + } satisfies CoolifyInstanceInfo, + firewallCpu: { total: 23 } satisfies FirewallCpuSummary, + firewallInterfaces: [ + { + data: [ + { name: "WAN", receive: 18_400_000, transmit: 4_200_000 }, + { name: "LAN", receive: 46_700_000, transmit: 21_300_000 }, + ], + timestamp: new Date(demoTimestamp), + }, + ] satisfies FirewallInterfacesSummary[], + firewallMemory: { used: 3_220_000_000, total: 8_590_000_000, percent: 37.5 } satisfies FirewallMemorySummary, + firewallVersion: { version: "24.7.11" } satisfies FirewallVersionSummary, + immichStats: { + userCount: 4, + photoCount: 48_392, + videoCount: 2_418, + totalLibraryUsageInBytes: 684_510_412_800, + } satisfies ImmichServerStats, + immichAlbums: [ + { id: "demo-paris", albumName: "Paris weekend", assetCount: 128 }, + { id: "demo-mountains", albumName: "Alpine trails", assetCount: 84 }, + ], + immichAlbum: { + id: "demo-paris", + albumName: "Paris weekend", + assets: [ + { + id: "demo-photo-1", + thumbhash: null, + fileModifiedAt: demoTimestamp, + fileCreatedAt: demoTimestamp, + updatedAt: demoTimestamp, + type: "IMAGE", + publicLink: "https://images.unsplash.com/photo-1502602898657-3e91760cbb34?auto=format&fit=crop&w=1200&q=80", + }, + { + id: "demo-photo-2", + thumbhash: null, + fileModifiedAt: demoTimestamp, + fileCreatedAt: demoTimestamp, + updatedAt: demoTimestamp, + type: "IMAGE", + publicLink: "https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?auto=format&fit=crop&w=1200&q=80", + }, + ], + } satisfies ImmichAlbum, + paperlessNgx: { + documentsTotal: 2_847, + documentsInbox: 14, + correspondentsCount: 96, + tagsCount: 38, + documentTypesCount: 12, + } satisfies PaperlessNgxStats, + patchmon: { + totalHosts: 18, + hostsNeedingUpdates: 5, + securityUpdates: 7, + upToDateHosts: 13, + hostsWithSecurityUpdates: 3, + recentUpdates24h: 11, + totalOutdatedPackages: 26, + totalRepos: 42, + lastUpdated: demoTimestamp, + osDistribution: [ + { name: "Ubuntu 24.04", count: 9, osType: "linux", osVersion: "24.04" }, + { name: "Debian 13", count: 6, osType: "linux", osVersion: "13" }, + { name: "Fedora 42", count: 3, osType: "linux", osVersion: "42" }, + ], + } satisfies PatchMonStats, + speedtestTracker: { + latestResult: { + id: 42, + ping: 8.4, + download_bits: 936_000_000, + upload_bits: 487_000_000, + healthy: true, + created_at: new Date(demoTimestamp), + }, + stats: { + ping: { avg: 9.1, min: 6.8, max: 14.2 }, + download: { avg: 901, avg_bits: 901_000_000, min: 822, max: 948 }, + upload: { avg: 472, avg_bits: 472_000_000, min: 431, max: 495 }, + total_results: 128, + }, + recentResults: [ + { + id: 42, + ping: 8.4, + download_bits: 936_000_000, + upload_bits: 487_000_000, + healthy: true, + created_at: new Date(demoTimestamp), + }, + ], + } satisfies SpeedtestTrackerDashboardData, + tracearr: { + stats: { activeStreams: 2, totalUsers: 14, totalSessions: 1_248, recentViolations: 1, timestamp: demoTimestamp }, + streams: { + data: [], + summary: { + total: 2, + transcodes: 1, + directStreams: 0, + directPlays: 1, + totalBitrate: "24.8 Mbps", + byServer: [ + { + serverId: "demo", + serverName: "Homarr Media", + total: 2, + transcodes: 1, + directStreams: 0, + directPlays: 1, + totalBitrate: "24.8 Mbps", + }, + ], + }, + }, + violations: { data: [], meta: { total: 1, page: 1, pageSize: 10 } }, + recentActivity: { data: [], meta: { total: 1_248, page: 1, pageSize: 10 } }, + } satisfies TracearrDashboardData, + traefik: { + version: "3.5.0", + entryPoints: ["web", "websecure", "metrics"], + failedEndpoints: [], + resources: [ + { protocol: "http", type: "router", name: "homarr@docker", provider: "docker", status: "enabled" }, + { protocol: "http", type: "service", name: "homarr@docker", provider: "docker", status: "enabled" }, + { protocol: "http", type: "middleware", name: "secure-headers@file", provider: "file", status: "enabled" }, + ], + http: { + routers: { total: 12, enabled: 12, warnings: 0, errors: 0 }, + services: { total: 10, enabled: 10, warnings: 0, errors: 0 }, + middlewares: { total: 8, enabled: 8, warnings: 0, errors: 0 }, + }, + tcp: { + routers: { total: 2, enabled: 2, warnings: 0, errors: 0 }, + services: { total: 2, enabled: 2, warnings: 0, errors: 0 }, + middlewares: { total: 0, enabled: 0, warnings: 0, errors: 0 }, + }, + udp: { + routers: { total: 1, enabled: 1, warnings: 0, errors: 0 }, + services: { total: 1, enabled: 1, warnings: 0, errors: 0 }, + }, + } satisfies TraefikDashboardData, + umamiWebsites: [{ id: "homarr-demo", name: "Homarr demo", domain: "demo.homarr.dev" }] satisfies UmamiWebsite[], + umamiVisitorStats: { + domain: "demo.homarr.dev", + websiteId: "homarr-demo", + websiteName: "Homarr demo", + totalVisitors: 8_429, + totalPageviews: 21_384, + totalVisits: 11_207, + bounceRate: 32.4, + avgDuration: 284, + timeFrame: "24h", + dataPoints: [ + { timestamp: "2026-08-24T08:00:00.000Z", visitors: 280, events: 0 }, + { timestamp: "2026-08-24T10:00:00.000Z", visitors: 410, events: 0 }, + { timestamp: "2026-08-24T12:00:00.000Z", visitors: 536, events: 0 }, + ], + eventCount: 0, + } satisfies UmamiVisitorStats, + umamiEventNames: ["dashboard_opened", "widget_added", "board_shared"], + umamiTopPages: [ + { x: "/", y: 8_240 }, + { x: "/boards/home", y: 5_812 }, + { x: "/manage/integrations", y: 1_942 }, + ] satisfies UmamiMetricItem[], + umamiTopReferrers: [ + { x: "github.com", y: 3_842 }, + { x: "google.com", y: 2_106 }, + { x: "docs.homarr.dev", y: 1_284 }, + ] satisfies UmamiMetricItem[], + umamiEventSeries: [ + { + eventName: "dashboard_opened", + dataPoints: [ + { x: "2026-08-24T08:00:00.000Z", y: 92 }, + { x: "2026-08-24T10:00:00.000Z", y: 134 }, + { x: "2026-08-24T12:00:00.000Z", y: 188 }, + ], + }, + ] satisfies UmamiEventSeries[], + uptimeKuma: { + totalMonitors: 6, + upCount: 5, + downCount: 1, + pausedCount: 0, + averageUptimePercent: 99.72, + monitors: [ + { id: 1, name: "Homarr", status: "up", uptimePercent24h: 100 }, + { id: 2, name: "Media server", status: "up", uptimePercent24h: 99.98 }, + { id: 3, name: "Home cloud", status: "up", uptimePercent24h: 99.91 }, + { id: 4, name: "Backups", status: "up", uptimePercent24h: 99.62 }, + { id: 5, name: "VPN", status: "up", uptimePercent24h: 99.8 }, + { id: 6, name: "Lab service", status: "down", uptimePercent24h: 98.99 }, + ], + } satisfies UptimeKumaDashboardData, + vpn: { + vpnStatus: "running", + dnsStatus: "running", + publicIp: "198.51.100.42", + country: "France", + city: "Paris", + vpnProvider: { protocol: "wireguard", provider: "Mullvad" }, + } satisfies VpnSummary, + wud: { + totalContainers: 24, + updatesAvailable: 3, + updates: [ + { + id: "homarr", + name: "Homarr", + currentVersion: "v1.35.0", + newVersion: "v1.36.0", + link: "https://github.com/homarr-labs/homarr", + }, + { id: "postgres", name: "Postgres", currentVersion: "17.5", newVersion: "17.6", link: null }, + { id: "redis", name: "Redis", currentVersion: "8.0.2", newVersion: "8.0.3", link: null }, + ], + } satisfies WudStats, +} as const; diff --git a/packages/widgets/src/anchor-note/index.ts b/packages/widgets/src/anchor-note/index.ts index 4eac1a441..d7a155911 100644 --- a/packages/widgets/src/anchor-note/index.ts +++ b/packages/widgets/src/anchor-note/index.ts @@ -8,7 +8,7 @@ export const { definition, componentLoader } = createWidgetDefinition("anchorNot supportsAdvancedFocus: true, queryKey: [["widget", "anchorNotes"]], maxIntegrations: 1, - supportedIntegrations: ["anchor"], + supportedIntegrations: ["anchor", "mock"], createOptions() { return optionsBuilder.from((factory) => ({ noteId: factory.anchorNote(), diff --git a/packages/widgets/src/archive-team-warrior/index.ts b/packages/widgets/src/archive-team-warrior/index.ts index 38a3eef36..f91f2d0ac 100644 --- a/packages/widgets/src/archive-team-warrior/index.ts +++ b/packages/widgets/src/archive-team-warrior/index.ts @@ -14,5 +14,5 @@ export const { definition, componentLoader } = createWidgetDefinition("archiveTe }), })); }, - supportedIntegrations: ["archiveTeamWarrior"], + supportedIntegrations: ["archiveTeamWarrior", "mock"], }).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/audio-stats/index.ts b/packages/widgets/src/audio-stats/index.ts index 6fb3b2014..836a2d6f6 100644 --- a/packages/widgets/src/audio-stats/index.ts +++ b/packages/widgets/src/audio-stats/index.ts @@ -4,7 +4,8 @@ import { createWidgetDefinition, matchesWidgetRuntimeQuery } from "../definition import { optionsBuilder } from "../options"; const hideUnlessNavidrome = { - shouldHide: (_: unknown, integrationKinds: string[]) => !integrationKinds.includes("navidrome"), + shouldHide: (_: unknown, integrationKinds: string[]) => + !integrationKinds.includes("navidrome") && !integrationKinds.includes("mock"), }; const hideUnlessAudiobookshelf = { @@ -16,7 +17,7 @@ export const { definition, componentLoader } = createWidgetDefinition("audioStat supportsAdvancedFocus: true, queryKeys: [[["widget", "audioStats", "getStats"]], [["widget", "mediaServer", "getCurrentStreams"]]], queryMatcher: matchesWidgetRuntimeQuery, - supportedIntegrations: ["navidrome", "audiobookshelf"], + supportedIntegrations: ["navidrome", "audiobookshelf", "mock"], integrationsRequired: true, maxIntegrations: 1, createOptions() { diff --git a/packages/widgets/src/bazarr/index.ts b/packages/widgets/src/bazarr/index.ts index ff5bb0d85..ec8e68f15 100644 --- a/packages/widgets/src/bazarr/index.ts +++ b/packages/widgets/src/bazarr/index.ts @@ -6,7 +6,7 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("bazarr", { supportsAdvancedFocus: true, icon: IconBadgeCc, - supportedIntegrations: ["bazarr"], + supportedIntegrations: ["bazarr", "mock"], integrationsRequired: true, maxIntegrations: 1, createOptions() { diff --git a/packages/widgets/src/coolify/index.ts b/packages/widgets/src/coolify/index.ts index cfc96bc9a..2c1ab2d8d 100644 --- a/packages/widgets/src/coolify/index.ts +++ b/packages/widgets/src/coolify/index.ts @@ -19,7 +19,7 @@ export const { definition, componentLoader } = createWidgetDefinition("coolify", }), })); }, - supportedIntegrations: ["coolify"], + supportedIntegrations: ["coolify", "mock"], errors: { INTERNAL_SERVER_ERROR: { icon: IconServerOff, diff --git a/packages/widgets/src/firewall/index.ts b/packages/widgets/src/firewall/index.ts index 7a013f07f..f48c37dff 100644 --- a/packages/widgets/src/firewall/index.ts +++ b/packages/widgets/src/firewall/index.ts @@ -12,7 +12,7 @@ export const { definition, componentLoader } = createWidgetDefinition("firewall" createOptions() { return optionsBuilder.from(() => ({})); }, - supportedIntegrations: getIntegrationKindsByCategory("firewall"), + supportedIntegrations: [...getIntegrationKindsByCategory("firewall"), "mock"], errors: { INTERNAL_SERVER_ERROR: { icon: IconWallOff, diff --git a/packages/widgets/src/immich/album-carousel/index.ts b/packages/widgets/src/immich/album-carousel/index.ts index d95c4d029..14e1c46e2 100644 --- a/packages/widgets/src/immich/album-carousel/index.ts +++ b/packages/widgets/src/immich/album-carousel/index.ts @@ -62,7 +62,7 @@ export const { definition, componentLoader } = createWidgetDefinition("immich-al : undefined, }), refetchInterval: null, - supportedIntegrations: ["immich"], + supportedIntegrations: ["immich", "mock"], integrationsRequired: true, maxIntegrations: 1, contextActions: ({ widgetRuntimeRef }) => { diff --git a/packages/widgets/src/immich/server-stats/index.ts b/packages/widgets/src/immich/server-stats/index.ts index dedd420fe..c1e45a68a 100644 --- a/packages/widgets/src/immich/server-stats/index.ts +++ b/packages/widgets/src/immich/server-stats/index.ts @@ -9,7 +9,7 @@ export const { definition, componentLoader } = createWidgetDefinition("immich-se queryKeys: [[["widget", "immich", "getServerStats"]], [["widget", "immich", "getAlbums"]]], queryMatcher: matchesWidgetRuntimeQuery, refetchInterval: null, - supportedIntegrations: ["immich"], + supportedIntegrations: ["immich", "mock"], integrationsRequired: true, maxIntegrations: 1, createOptions() { diff --git a/packages/widgets/src/paperless-ngx/index.ts b/packages/widgets/src/paperless-ngx/index.ts index 600ee4652..ac4519677 100644 --- a/packages/widgets/src/paperless-ngx/index.ts +++ b/packages/widgets/src/paperless-ngx/index.ts @@ -7,7 +7,7 @@ export const { definition, componentLoader } = createWidgetDefinition("paperless supportsAdvancedFocus: false, icon: IconFileText, refetchInterval: null, - supportedIntegrations: ["paperlessNgx"], + supportedIntegrations: ["paperlessNgx", "mock"], integrationsRequired: true, maxIntegrations: 1, createOptions() { diff --git a/packages/widgets/src/patchmon/index.ts b/packages/widgets/src/patchmon/index.ts index d715e90ac..46688b3cf 100644 --- a/packages/widgets/src/patchmon/index.ts +++ b/packages/widgets/src/patchmon/index.ts @@ -9,7 +9,7 @@ import { patchmonOptionsSuperRefine } from "./threshold-validation"; export const { definition, componentLoader } = createWidgetDefinition("patchmon", { supportsAdvancedFocus: true, icon: IconShieldCheck, - supportedIntegrations: ["patchmon"], + supportedIntegrations: ["patchmon", "mock"], integrationsRequired: true, maxIntegrations: 1, createOptions() { diff --git a/packages/widgets/src/speedtest-tracker/index.ts b/packages/widgets/src/speedtest-tracker/index.ts index 7de41100f..81ddd8f3f 100644 --- a/packages/widgets/src/speedtest-tracker/index.ts +++ b/packages/widgets/src/speedtest-tracker/index.ts @@ -30,7 +30,7 @@ export const { definition, componentLoader } = createWidgetDefinition("speedtest }, ); }, - supportedIntegrations: ["speedtestTracker"], + supportedIntegrations: ["speedtestTracker", "mock"], errors: { INTERNAL_SERVER_ERROR: { icon: IconServerOff, diff --git a/packages/widgets/src/system-disks/index.ts b/packages/widgets/src/system-disks/index.ts index 0780d1e6d..18671bf7f 100644 --- a/packages/widgets/src/system-disks/index.ts +++ b/packages/widgets/src/system-disks/index.ts @@ -9,7 +9,7 @@ export const { definition, componentLoader } = createWidgetDefinition("systemDis icon: IconServer2, queryKey: [["widget", "healthMonitoring"]], refetchInterval: 10, - supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid", "synology"], + supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid", "synology", "mock"], createOptions() { return optionsBuilder.from( (factory) => ({ diff --git a/packages/widgets/src/system-resources/index.ts b/packages/widgets/src/system-resources/index.ts index 03d7a83db..6d87c1e6b 100644 --- a/packages/widgets/src/system-resources/index.ts +++ b/packages/widgets/src/system-resources/index.ts @@ -24,7 +24,7 @@ export const { definition, componentLoader } = createWidgetDefinition("systemRes supportsAdvancedFocus: true, queryKey: [["widget", "healthMonitoring"]], refetchInterval: 10, - supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid", "glances", "synology"], + supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid", "glances", "synology", "mock"], createOptions() { return optionsBuilder.from((factory) => ({ hasShadow: factory.switch({ defaultValue: true }), diff --git a/packages/widgets/src/tracearr/index.ts b/packages/widgets/src/tracearr/index.ts index 61f88cf4f..6a17f3fec 100644 --- a/packages/widgets/src/tracearr/index.ts +++ b/packages/widgets/src/tracearr/index.ts @@ -24,7 +24,7 @@ export const { definition, componentLoader } = createWidgetDefinition("tracearr" }), })); }, - supportedIntegrations: ["tracearr"], + supportedIntegrations: ["tracearr", "mock"], errors: { INTERNAL_SERVER_ERROR: { icon: IconServerOff, diff --git a/packages/widgets/src/traefik/index.ts b/packages/widgets/src/traefik/index.ts index 95392a152..431c4a462 100644 --- a/packages/widgets/src/traefik/index.ts +++ b/packages/widgets/src/traefik/index.ts @@ -19,7 +19,7 @@ export const { definition, componentLoader } = createWidgetDefinition("traefik", }), })); }, - supportedIntegrations: ["traefik"], + supportedIntegrations: ["traefik", "mock"], errors: { INTERNAL_SERVER_ERROR: { icon: IconRouteOff, diff --git a/packages/widgets/src/umami/index.ts b/packages/widgets/src/umami/index.ts index 9c96e4a8e..b4a5a7d0e 100644 --- a/packages/widgets/src/umami/index.ts +++ b/packages/widgets/src/umami/index.ts @@ -18,7 +18,7 @@ export const { definition, componentLoader } = createWidgetDefinition("umami", { ], queryMatcher: matchesWidgetRuntimeQuery, refetchInterval: null, - supportedIntegrations: ["umami"], + supportedIntegrations: ["umami", "mock"], integrationsRequired: true, maxIntegrations: 1, createOptions() { diff --git a/packages/widgets/src/uptime-kuma/index.ts b/packages/widgets/src/uptime-kuma/index.ts index 0b0cff5ad..f5274f6c6 100644 --- a/packages/widgets/src/uptime-kuma/index.ts +++ b/packages/widgets/src/uptime-kuma/index.ts @@ -34,7 +34,7 @@ export const { definition, componentLoader } = createWidgetDefinition("uptimeKum }), })); }, - supportedIntegrations: ["uptimeKuma"], + supportedIntegrations: ["uptimeKuma", "mock"], errors: { INTERNAL_SERVER_ERROR: { icon: IconServerOff, diff --git a/packages/widgets/src/vpn/index.ts b/packages/widgets/src/vpn/index.ts index f8418d86f..001f2eec0 100644 --- a/packages/widgets/src/vpn/index.ts +++ b/packages/widgets/src/vpn/index.ts @@ -12,5 +12,5 @@ export const { componentLoader, definition } = createWidgetDefinition("vpn", { createOptions() { return optionsBuilder.from(() => ({})); }, - supportedIntegrations: getIntegrationKindsByCategory("vpn"), + supportedIntegrations: [...getIntegrationKindsByCategory("vpn"), "mock"], }).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/wud/index.ts b/packages/widgets/src/wud/index.ts index d7816b480..622b10b6b 100644 --- a/packages/widgets/src/wud/index.ts +++ b/packages/widgets/src/wud/index.ts @@ -7,7 +7,7 @@ export const { definition, componentLoader } = createWidgetDefinition("wud", { icon: IconBrandDocker, supportsAdvancedFocus: true, refetchInterval: null, - supportedIntegrations: ["wud"], + supportedIntegrations: ["wud", "mock"], integrationsRequired: true, maxIntegrations: 1, createOptions() {