Compare commits

...
Author SHA1 Message Date
ajnart 245a477965 refactor: simplify demo media 2026-08-24 12:07:07 +02:00
ajnart d8eec36d98 fix: address demo mock review 2026-08-24 10:27:28 +02:00
ajnart 135611bb50 fix: satisfy mock router lint 2026-08-24 10:22:18 +02:00
ajnart 71c328cd7a feat: mock all demo widgets 2026-08-24 10:13:49 +02:00
42 changed files with 939 additions and 163 deletions

No files matched your search

@@ -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 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 ```sh
pnpm cli harness port feature-board pnpm cli harness port feature-board
``` ```
@@ -3,7 +3,12 @@ import { z } from "zod/v4";
import { ResponseError } from "@homarr/common/server"; import { ResponseError } from "@homarr/common/server";
import { createLogger } from "@homarr/core/infrastructure/logs"; 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 { anchorNoteRequestHandler, anchorNotesListRequestHandler } from "@homarr/request-handler/anchor-notes";
import { createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createOneIntegrationMiddleware } from "../../middlewares/integration";
@@ -15,6 +20,12 @@ const noteIdInput = z.object({
const logger = createLogger({ module: "anchorNotesRouter" }); 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) => { const isJsonDeltaString = (value: string) => {
try { try {
const parsed: unknown = JSON.parse(value); const parsed: unknown = JSON.parse(value);
@@ -40,34 +51,64 @@ const normalizeAnchorContent = (content: string | undefined) => {
export const anchorNotesRouter = createTRPCRouter({ export const anchorNotesRouter = createTRPCRouter({
listNotes: publicProcedure listNotes: publicProcedure
.concat(createOneIntegrationMiddleware("query", "anchor")) .concat(createOneIntegrationMiddleware("query", "anchor", "mock"))
.input(anchorNotesListInputSchema) .input(anchorNotesListInputSchema)
.query(async ({ ctx, input }) => { .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(); const { data } = await handler.getDataAsync();
return data; return data;
}), }),
getNote: publicProcedure getNote: publicProcedure
.concat(createOneIntegrationMiddleware("query", "anchor")) .concat(createOneIntegrationMiddleware("query", "anchor", "mock"))
.input(noteIdInput) .input(noteIdInput)
.query(async ({ ctx, input }) => { .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(); const { data } = await handler.getDataAsync();
return data; return data;
}), }),
updateNote: protectedProcedure updateNote: protectedProcedure
.concat(createOneIntegrationMiddleware("interact", "anchor")) .concat(createOneIntegrationMiddleware("interact", "anchor", "mock"))
.input(anchorNoteUpdateInputSchema) .input(anchorNoteUpdateInputSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const integrationInstance = await createIntegrationAsync(ctx.integration);
const normalizedContent = normalizeAnchorContent(input.content); const normalizedContent = normalizeAnchorContent(input.content);
const isContentNormalized = const isContentNormalized =
input.content !== undefined && normalizedContent !== undefined && input.content !== normalizedContent; 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 { try {
const updatedNote = await integrationInstance.updateNoteAsync({ const updatedNote = await integrationInstance.updateNoteAsync({
noteId: input.noteId, noteId: input.noteId,
@@ -1,13 +1,18 @@
import { archiveTeamWarriorRequestHandler } from "@homarr/request-handler/archive-team-warrior"; import { archiveTeamWarriorRequestHandler } from "@homarr/request-handler/archive-team-warrior";
import { mockWidgetData } from "@homarr/integrations";
import { createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc"; import { createTRPCRouter, publicProcedure } from "../../trpc";
export const archiveTeamWarriorRouter = createTRPCRouter({ export const archiveTeamWarriorRouter = createTRPCRouter({
getStatus: publicProcedure getStatus: publicProcedure
.concat(createOneIntegrationMiddleware("query", "archiveTeamWarrior")) .concat(createOneIntegrationMiddleware("query", "archiveTeamWarrior", "mock"))
.query(async ({ ctx }) => { .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(); const { data, timestamp } = await handler.getDataAsync();
return { return {
@@ -1,14 +1,16 @@
import { audiobookshelfRequestHandler } from "@homarr/request-handler/audiobookshelf"; import { audiobookshelfRequestHandler } from "@homarr/request-handler/audiobookshelf";
import { navidromeRequestHandler } from "@homarr/request-handler/navidrome"; import { navidromeRequestHandler } from "@homarr/request-handler/navidrome";
import { mockWidgetData } from "@homarr/integrations";
import { createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc"; import { createTRPCRouter, publicProcedure } from "../../trpc";
export const audioStatsRouter = createTRPCRouter({ export const audioStatsRouter = createTRPCRouter({
getStats: publicProcedure getStats: publicProcedure
.concat(createOneIntegrationMiddleware("query", "navidrome", "audiobookshelf")) .concat(createOneIntegrationMiddleware("query", "navidrome", "audiobookshelf", "mock"))
.query(async ({ ctx }) => { .query(async ({ ctx }) => {
const { kind } = ctx.integration; const { kind } = ctx.integration;
if (kind === "mock") return { kind: "navidrome" as const, data: mockWidgetData.audioStats };
const fetchByKind = { const fetchByKind = {
navidrome: async () => { navidrome: async () => {
+5 -2
View File
@@ -1,4 +1,5 @@
import { bazarrBadgesRequestHandler } from "@homarr/request-handler/bazarr"; import { bazarrBadgesRequestHandler } from "@homarr/request-handler/bazarr";
import { mockWidgetData } from "@homarr/integrations";
import { createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc"; 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", "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 }) => { .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(); const data = await innerHandler.getDataAsync();
return data.data; return data.data;
}), }),
+13 -2
View File
@@ -1,4 +1,5 @@
import type { CoolifyInstanceInfo } from "@homarr/integrations/types"; import type { CoolifyInstanceInfo } from "@homarr/integrations/types";
import { mockWidgetData } from "@homarr/integrations";
import { coolifyRequestHandler } from "@homarr/request-handler/coolify"; import { coolifyRequestHandler } from "@homarr/request-handler/coolify";
import { createManyIntegrationMiddleware } from "../../middlewares/integration"; import { createManyIntegrationMiddleware } from "../../middlewares/integration";
@@ -16,12 +17,22 @@ interface CoolifyInstanceResult {
export const coolifyRouter = createTRPCRouter({ export const coolifyRouter = createTRPCRouter({
getInstancesInfo: publicProcedure getInstancesInfo: publicProcedure
.concat(createManyIntegrationMiddleware("query", "coolify")) .concat(createManyIntegrationMiddleware("query", "coolify", "mock"))
.query(async ({ ctx }) => { .query(async ({ ctx }) => {
return await settleIntegrationQueries( return await settleIntegrationQueries(
ctx.integrations, ctx.integrations,
async (integration): Promise<CoolifyInstanceResult> => { async (integration): Promise<CoolifyInstanceResult> => {
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(); const { data, timestamp } = await innerHandler.getDataAsync();
return { return {
+58 -16
View File
@@ -1,4 +1,6 @@
import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions";
import type { IntegrationKind } from "@homarr/definitions";
import { mockWidgetData } from "@homarr/integrations";
import { import {
firewallCpuRequestHandler, firewallCpuRequestHandler,
firewallInterfacesRequestHandler, firewallInterfacesRequestHandler,
@@ -10,29 +12,53 @@ import { createManyIntegrationMiddleware } from "../../middlewares/integration";
import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations"; import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations";
import { createTRPCRouter, publicProcedure } from "../../trpc"; import { createTRPCRouter, publicProcedure } from "../../trpc";
const firewallMiddleware = createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall")); const firewallMiddleware = createManyIntegrationMiddleware(
"query",
...getIntegrationKindsByCategory("firewall"),
"mock",
);
const queryFirewall = < interface FirewallResult<TSummary> {
THandler extends { integration: { id: string; name: string; kind: IntegrationKind; updatedAt: Date };
handler: (integration: any, input: any) => { getDataAsync: () => Promise<{ data: any; timestamp: Date }> }; summary: TSummary;
error?: string;
}
const queryFirewall = <TSummary>(
handler: {
handler: (
integration: any,
input: Record<string, never>,
) => { getDataAsync: () => Promise<{ data: TSummary; timestamp: Date }> };
}, },
>( createFallback: () => TSummary,
handler: THandler, getMockData: () => TSummary,
createFallback: () => Awaited<ReturnType<ReturnType<THandler["handler"]>["getDataAsync"]>>["data"],
) => ) =>
publicProcedure.concat(firewallMiddleware).query(async ({ ctx }) => publicProcedure.concat(firewallMiddleware).query(async ({ ctx }) =>
settleIntegrationQueries( settleIntegrationQueries(
ctx.integrations, ctx.integrations,
async (integration) => { async (integration): Promise<FirewallResult<TSummary>> => {
const { data, timestamp } = await handler.handler(integration, {}).getDataAsync(); 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 { return {
integration: { id: integration.id, name: integration.name, kind: integration.kind, updatedAt: timestamp }, integration: { id: integration.id, name: integration.name, kind: integration.kind, updatedAt: timestamp },
summary: data as Awaited<ReturnType<ReturnType<THandler["handler"]>["getDataAsync"]>>["data"], summary: data,
error: undefined as string | undefined, error: undefined,
}; };
}, },
{ {
fallback: (integration, error) => ({ fallback: (integration, error): FirewallResult<TSummary> => ({
integration: { id: integration.id, name: integration.name, kind: integration.kind, updatedAt: new Date(0) }, integration: { id: integration.id, name: integration.name, kind: integration.kind, updatedAt: new Date(0) },
summary: createFallback(), summary: createFallback(),
error: toPublicIntegrationError(error), error: toPublicIntegrationError(error),
@@ -43,8 +69,24 @@ const queryFirewall = <
); );
export const firewallRouter = createTRPCRouter({ export const firewallRouter = createTRPCRouter({
getFirewallCpuStatus: queryFirewall(firewallCpuRequestHandler, () => ({ total: 0 })), getFirewallCpuStatus: queryFirewall(
getFirewallInterfacesStatus: queryFirewall(firewallInterfacesRequestHandler, () => []), firewallCpuRequestHandler,
getFirewallVersionStatus: queryFirewall(firewallVersionRequestHandler, () => ({ version: "Unknown" })), () => ({ total: 0 }),
getFirewallMemoryStatus: queryFirewall(firewallMemoryRequestHandler, () => ({ used: 0, total: 0, percent: 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,
),
}); });
+18 -4
View File
@@ -1,6 +1,7 @@
import { z } from "zod/v4"; import { z } from "zod/v4";
import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { mockWidgetData } from "@homarr/integrations";
import { import {
immichAlbumRequestHandler, immichAlbumRequestHandler,
immichAlbumsRequestHandler, immichAlbumsRequestHandler,
@@ -12,11 +13,12 @@ import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc"; import { createTRPCRouter, publicProcedure } from "../../trpc";
const createImmichIntegrationMiddleware = (action: IntegrationAction) => const createImmichIntegrationMiddleware = (action: IntegrationAction) =>
createOneIntegrationMiddleware(action, ...getIntegrationKindsByCategory("photoService")); createOneIntegrationMiddleware(action, ...getIntegrationKindsByCategory("photoService"), "mock");
export const immichRouter = createTRPCRouter({ export const immichRouter = createTRPCRouter({
getServerStats: publicProcedure.concat(createImmichIntegrationMiddleware("query")).query(async ({ ctx }) => { 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(); const data = await innerHandler.getDataAsync();
return data.data; return data.data;
}), }),
@@ -29,7 +31,13 @@ export const immichRouter = createTRPCRouter({
}), }),
) )
.query(async ({ ctx, input }) => { .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(); const data = await innerHandler.getDataAsync();
return data.data; return data.data;
}), }),
@@ -42,7 +50,13 @@ export const immichRouter = createTRPCRouter({
) )
.concat(createImmichIntegrationMiddleware("query")) .concat(createImmichIntegrationMiddleware("query"))
.query(async ({ ctx, input }) => { .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(); const data = await innerHandler.getDataAsync();
return data.data; return data.data;
}), }),
@@ -1,12 +1,16 @@
import { paperlessNgxStatsRequestHandler } from "@homarr/request-handler/paperless-ngx"; import { paperlessNgxStatsRequestHandler } from "@homarr/request-handler/paperless-ngx";
import { mockWidgetData } from "@homarr/integrations";
import { createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc"; import { createTRPCRouter, publicProcedure } from "../../trpc";
export const paperlessNgxRouter = createTRPCRouter({ export const paperlessNgxRouter = createTRPCRouter({
getStats: publicProcedure.concat(createOneIntegrationMiddleware("query", "paperlessNgx")).query(async ({ ctx }) => { getStats: publicProcedure
const innerHandler = paperlessNgxStatsRequestHandler.handler(ctx.integration, {}); .concat(createOneIntegrationMiddleware("query", "paperlessNgx", "mock"))
const data = await innerHandler.getDataAsync(); .query(async ({ ctx }) => {
return data.data; if (ctx.integration.kind === "mock") return mockWidgetData.paperlessNgx;
}), const innerHandler = paperlessNgxStatsRequestHandler.handler({ ...ctx.integration, kind: "paperlessNgx" }, {});
const data = await innerHandler.getDataAsync();
return data.data;
}),
}); });
+4 -2
View File
@@ -1,4 +1,5 @@
import { patchmonStatsRequestHandler } from "@homarr/request-handler/patchmon"; import { patchmonStatsRequestHandler } from "@homarr/request-handler/patchmon";
import { mockWidgetData } from "@homarr/integrations";
import { createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, protectedProcedure } from "../../trpc"; 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.", "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 }) => { .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(); const { data } = await innerHandler.getDataAsync();
return data; return data;
}), }),
@@ -1,4 +1,5 @@
import { speedtestTrackerRequestHandler } from "@homarr/request-handler/speedtest-tracker"; import { speedtestTrackerRequestHandler } from "@homarr/request-handler/speedtest-tracker";
import { mockWidgetData } from "@homarr/integrations";
import { createManyIntegrationMiddleware } from "../../middlewares/integration"; import { createManyIntegrationMiddleware } from "../../middlewares/integration";
import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations"; import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations";
@@ -6,12 +7,22 @@ import { createTRPCRouter, publicProcedure } from "../../trpc";
export const speedtestTrackerRouter = createTRPCRouter({ export const speedtestTrackerRouter = createTRPCRouter({
getDashboard: publicProcedure getDashboard: publicProcedure
.concat(createManyIntegrationMiddleware("query", "speedtestTracker")) .concat(createManyIntegrationMiddleware("query", "speedtestTracker", "mock"))
.query(async ({ ctx }) => { .query(async ({ ctx }) => {
return await settleIntegrationQueries( return await settleIntegrationQueries(
ctx.integrations, ctx.integrations,
async (integration) => { 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(); const { data, timestamp } = await innerHandler.getDataAsync();
return { return {
+41 -28
View File
@@ -1,37 +1,50 @@
import { tracearrRequestHandler } from "@homarr/request-handler/tracearr"; import { tracearrRequestHandler } from "@homarr/request-handler/tracearr";
import { mockWidgetData } from "@homarr/integrations";
import { createManyIntegrationMiddleware } from "../../middlewares/integration"; import { createManyIntegrationMiddleware } from "../../middlewares/integration";
import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations"; import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations";
import { createTRPCRouter, publicProcedure } from "../../trpc"; import { createTRPCRouter, publicProcedure } from "../../trpc";
export const tracearrRouter = createTRPCRouter({ export const tracearrRouter = createTRPCRouter({
getDashboard: publicProcedure.concat(createManyIntegrationMiddleware("query", "tracearr")).query(async ({ ctx }) => { getDashboard: publicProcedure
return await settleIntegrationQueries( .concat(createManyIntegrationMiddleware("query", "tracearr", "mock"))
ctx.integrations, .query(async ({ ctx }) => {
async (integration) => { return await settleIntegrationQueries(
const innerHandler = tracearrRequestHandler.handler(integration, {}); ctx.integrations,
const { data, timestamp } = await innerHandler.getDataAsync(); 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 { return {
integrationId: integration.id, integrationId: integration.id,
integrationName: integration.name, integrationName: integration.name,
integrationUrl: integration.url, integrationUrl: integration.url,
dashboard: data as typeof data | null, dashboard: data as typeof data | null,
updatedAt: timestamp, updatedAt: timestamp,
error: undefined as string | undefined, error: undefined as string | undefined,
}; };
}, },
{ {
fallback: (integration, error) => ({ fallback: (integration, error) => ({
integrationId: integration.id, integrationId: integration.id,
integrationName: integration.name, integrationName: integration.name,
integrationUrl: integration.url, integrationUrl: integration.url,
dashboard: null, dashboard: null,
updatedAt: new Date(0), updatedAt: new Date(0),
error: toPublicIntegrationError(error), error: toPublicIntegrationError(error),
}), }),
throwOnAllFailures: true, throwOnAllFailures: true,
}, },
); );
}), }),
}); });
+13 -2
View File
@@ -1,4 +1,5 @@
import { traefikRequestHandler } from "@homarr/request-handler/traefik"; import { traefikRequestHandler } from "@homarr/request-handler/traefik";
import { mockWidgetData } from "@homarr/integrations";
import { createManyIntegrationMiddleware } from "../../middlewares/integration"; import { createManyIntegrationMiddleware } from "../../middlewares/integration";
import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations"; 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.", "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 }) => { .query(async ({ ctx }) => {
return await settleIntegrationQueries( return await settleIntegrationQueries(
ctx.integrations, ctx.integrations,
async (integration) => { 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(); const { data, timestamp } = await innerHandler.getDataAsync();
return { return {
+79 -40
View File
@@ -2,7 +2,7 @@ import { z } from "zod/v4";
import { createLogger } from "@homarr/core/infrastructure/logs"; import { createLogger } from "@homarr/core/infrastructure/logs";
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
import { createIntegrationAsync } from "@homarr/integrations"; import { createIntegrationAsync, mockWidgetData } from "@homarr/integrations";
import { import {
umamiActiveVisitorsRequestHandler, umamiActiveVisitorsRequestHandler,
umamiEventNamesRequestHandler, umamiEventNamesRequestHandler,
@@ -19,15 +19,18 @@ import { createTRPCRouter, publicProcedure } from "../../trpc";
const logger = createLogger({ module: "umami-router" }); const logger = createLogger({ module: "umami-router" });
export const umamiRouter = createTRPCRouter({ export const umamiRouter = createTRPCRouter({
getWebsites: publicProcedure.concat(createOneIntegrationMiddleware("query", "umami")).query(async ({ ctx }) => { getWebsites: publicProcedure
try { .concat(createOneIntegrationMiddleware("query", "umami", "mock"))
const instance = await createIntegrationAsync(ctx.integration); .query(async ({ ctx }) => {
return await instance.getWebsitesAsync(); if (ctx.integration.kind === "mock") return mockWidgetData.umamiWebsites;
} catch (error) { try {
logger.warn(new Error("Failed to load websites", { cause: error })); const instance = await createIntegrationAsync({ ...ctx.integration, kind: "umami" });
return []; return await instance.getWebsitesAsync();
} } catch (error) {
}), logger.warn(new Error("Failed to load websites", { cause: error }));
return [];
}
}),
getVisitorStats: publicProcedure getVisitorStats: publicProcedure
.input( .input(
@@ -37,14 +40,26 @@ export const umamiRouter = createTRPCRouter({
eventName: z.string().optional(), eventName: z.string().optional(),
}), }),
) )
.concat(createManyIntegrationMiddleware("query", "umami")) .concat(createManyIntegrationMiddleware("query", "umami", "mock"))
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
return await settleIntegrationQueries(ctx.integrations, async (integration) => { return await settleIntegrationQueries(ctx.integrations, async (integration) => {
const innerHandler = umamiRequestHandler.handler(integration, { if (integration.kind === "mock") {
websiteId: input.websiteId, return {
timeFrame: input.timeFrame, integrationId: integration.id,
eventName: input.eventName, 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(); const { data, timestamp } = await innerHandler.getDataAsync();
return { return {
@@ -59,10 +74,16 @@ export const umamiRouter = createTRPCRouter({
getEventNames: publicProcedure getEventNames: publicProcedure
.input(z.object({ websiteId: z.string() })) .input(z.object({ websiteId: z.string() }))
.concat(createOneIntegrationMiddleware("query", "umami")) .concat(createOneIntegrationMiddleware("query", "umami", "mock"))
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
if (ctx.integration.kind === "mock") return mockWidgetData.umamiEventNames;
try { 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(); const { data } = await innerHandler.getDataAsync();
return data; return data;
} catch (error) { } catch (error) {
@@ -81,14 +102,18 @@ export const umamiRouter = createTRPCRouter({
limit: z.number().int().min(1).max(500), limit: z.number().int().min(1).max(500),
}), }),
) )
.concat(createOneIntegrationMiddleware("query", "umami")) .concat(createOneIntegrationMiddleware("query", "umami", "mock"))
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
if (ctx.integration.kind === "mock") return mockWidgetData.umamiTopPages.slice(0, input.limit);
try { try {
const innerHandler = umamiTopPagesRequestHandler.handler(ctx.integration, { const innerHandler = umamiTopPagesRequestHandler.handler(
websiteId: input.websiteId, { ...ctx.integration, kind: "umami" },
timeFrame: input.timeFrame, {
limit: input.limit, websiteId: input.websiteId,
}); timeFrame: input.timeFrame,
limit: input.limit,
},
);
const { data } = await innerHandler.getDataAsync(); const { data } = await innerHandler.getDataAsync();
return data; return data;
} catch (error) { } catch (error) {
@@ -111,14 +136,18 @@ export const umamiRouter = createTRPCRouter({
limit: z.number().int().min(1).max(500), limit: z.number().int().min(1).max(500),
}), }),
) )
.concat(createOneIntegrationMiddleware("query", "umami")) .concat(createOneIntegrationMiddleware("query", "umami", "mock"))
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
if (ctx.integration.kind === "mock") return mockWidgetData.umamiTopReferrers.slice(0, input.limit);
try { try {
const innerHandler = umamiTopReferrersRequestHandler.handler(ctx.integration, { const innerHandler = umamiTopReferrersRequestHandler.handler(
websiteId: input.websiteId, { ...ctx.integration, kind: "umami" },
timeFrame: input.timeFrame, {
limit: input.limit, websiteId: input.websiteId,
}); timeFrame: input.timeFrame,
limit: input.limit,
},
);
const { data } = await innerHandler.getDataAsync(); const { data } = await innerHandler.getDataAsync();
return data; return data;
} catch (error) { } catch (error) {
@@ -141,15 +170,21 @@ export const umamiRouter = createTRPCRouter({
eventNames: z.array(z.string()), eventNames: z.array(z.string()),
}), }),
) )
.concat(createOneIntegrationMiddleware("query", "umami")) .concat(createOneIntegrationMiddleware("query", "umami", "mock"))
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
if (ctx.integration.kind === "mock") {
return mockWidgetData.umamiEventSeries.filter((series) => input.eventNames.includes(series.eventName));
}
try { try {
const sortedNames = [...input.eventNames].toSorted(); const sortedNames = [...input.eventNames].toSorted();
const innerHandler = umamiMultiEventRequestHandler.handler(ctx.integration, { const innerHandler = umamiMultiEventRequestHandler.handler(
websiteId: input.websiteId, { ...ctx.integration, kind: "umami" },
timeFrame: input.timeFrame, {
eventNames: sortedNames, websiteId: input.websiteId,
}); timeFrame: input.timeFrame,
eventNames: sortedNames,
},
);
const { data } = await innerHandler.getDataAsync(); const { data } = await innerHandler.getDataAsync();
return data; return data;
} catch (error) { } catch (error) {
@@ -166,12 +201,16 @@ export const umamiRouter = createTRPCRouter({
getActiveVisitors: publicProcedure getActiveVisitors: publicProcedure
.input(z.object({ websiteId: z.string() })) .input(z.object({ websiteId: z.string() }))
.concat(createOneIntegrationMiddleware("query", "umami")) .concat(createOneIntegrationMiddleware("query", "umami", "mock"))
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
if (ctx.integration.kind === "mock") return 7;
try { try {
const innerHandler = umamiActiveVisitorsRequestHandler.handler(ctx.integration, { const innerHandler = umamiActiveVisitorsRequestHandler.handler(
websiteId: input.websiteId, { ...ctx.integration, kind: "umami" },
}); {
websiteId: input.websiteId,
},
);
const { data } = await innerHandler.getDataAsync(); const { data } = await innerHandler.getDataAsync();
return data; return data;
} catch (error) { } catch (error) {
+13 -2
View File
@@ -1,4 +1,5 @@
import { uptimeKumaRequestHandler } from "@homarr/request-handler/uptime-kuma"; import { uptimeKumaRequestHandler } from "@homarr/request-handler/uptime-kuma";
import { mockWidgetData } from "@homarr/integrations";
import { createManyIntegrationMiddleware } from "../../middlewares/integration"; import { createManyIntegrationMiddleware } from "../../middlewares/integration";
import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations"; import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations";
@@ -6,12 +7,22 @@ import { createTRPCRouter, publicProcedure } from "../../trpc";
export const uptimeKumaRouter = createTRPCRouter({ export const uptimeKumaRouter = createTRPCRouter({
getDashboard: publicProcedure getDashboard: publicProcedure
.concat(createManyIntegrationMiddleware("query", "uptimeKuma")) .concat(createManyIntegrationMiddleware("query", "uptimeKuma", "mock"))
.query(async ({ ctx }) => { .query(async ({ ctx }) => {
return await settleIntegrationQueries( return await settleIntegrationQueries(
ctx.integrations, ctx.integrations,
async (integration) => { 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(); const { data, timestamp } = await innerHandler.getDataAsync();
return { return {
+27 -4
View File
@@ -1,18 +1,41 @@
import { getIntegrationKindsByCategory } from "@homarr/definitions"; 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 { vpnSummaryHandler } from "@homarr/request-handler/vpn";
import { createManyIntegrationMiddleware } from "../../middlewares/integration"; import { createManyIntegrationMiddleware } from "../../middlewares/integration";
import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations"; import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations";
import { createTRPCRouter, publicProcedure } from "../../trpc"; 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({ export const vpnRouter = createTRPCRouter({
getSummaries: publicProcedure getSummaries: publicProcedure
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("vpn"))) .unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("vpn"), "mock"))
.query(async ({ ctx }) => { .query(async ({ ctx }) => {
return await settleIntegrationQueries( return await settleIntegrationQueries(
ctx.integrations, ctx.integrations,
async (integration) => { async (integration): Promise<VpnResult> => {
const { data, timestamp } = await vpnSummaryHandler.handler(integration, {}).getDataAsync(); 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 { return {
integration: { integration: {
id: integration.id, id: integration.id,
@@ -25,7 +48,7 @@ export const vpnRouter = createTRPCRouter({
}; };
}, },
{ {
fallback: (integration, error) => ({ fallback: (integration, error): VpnResult => ({
integration: { integration: {
id: integration.id, id: integration.id,
name: integration.name, name: integration.name,
+7 -3
View File
@@ -1,4 +1,5 @@
import { wudStatsRequestHandler } from "@homarr/request-handler/wud"; import { wudStatsRequestHandler } from "@homarr/request-handler/wud";
import { mockWidgetData } from "@homarr/integrations";
import { createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc"; import { createTRPCRouter, publicProcedure } from "../../trpc";
@@ -9,12 +10,15 @@ export const wudRouter = createTRPCRouter({
mcp: { mcp: {
enabled: true, enabled: true,
description: 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 }) => { .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(); const { data, timestamp } = await handler.getDataAsync();
return { return {
+162
View File
@@ -730,6 +730,168 @@ const buildDemoWidgets = (appIds: string[], customWidgetDefinitionId: string): D
{ kind: "notifications", xOffset: 3, yOffset: 9, width: 2, height: 2, needsIntegration: true }, { kind: "notifications", xOffset: 3, yOffset: 9, width: 2, height: 2, needsIntegration: true },
{ kind: "beszelAlerts", xOffset: 5, yOffset: 9, width: 4, 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 // Right app rail
...appIds.map( ...appIds.map(
(appId, index): DemoWidget => ({ (appId, index): DemoWidget => ({
+15 -2
View File
@@ -7,6 +7,7 @@ import SuperJSON from "superjson";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import { DB_CASING } from "@homarr/core/infrastructure/db/constants"; import { DB_CASING } from "@homarr/core/infrastructure/db/constants";
import { widgetIntegrationSupport, widgetKinds } from "@homarr/definitions";
import type { Database } from ".."; import type { Database } from "..";
import { seedDataAsync } from "../migrations/seed"; import { seedDataAsync } from "../migrations/seed";
@@ -45,7 +46,7 @@ test("SQLite migrations seed the five disabled bundled custom widgets", async ()
connection.close(); 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; const previousDemoMode = process.env.DEMO_MODE;
process.env.DEMO_MODE = "true"; process.env.DEMO_MODE = "true";
const connection = new BetterSqlite3(":memory:"); const connection = new BetterSqlite3(":memory:");
@@ -65,7 +66,7 @@ test("SQLite migrations seed the redesigned demo dashboard", async () => {
with: { with: {
layouts: true, layouts: true,
sections: { with: { collapseStates: true, 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"); 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("notifications", 3, 9, 2, 2);
expectItemLayout("beszelAlerts", 5, 9, 4, 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"]) { for (const kind of ["weather", "airQuality"]) {
const item = itemByKind(kind); const item = itemByKind(kind);
if (!item?.options) throw new Error(`Demo ${kind} options were not seeded`); if (!item?.options) throw new Error(`Demo ${kind} options were not seeded`);
@@ -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", () => { test("derives optional connections and server selection limits", () => {
expect(widgetKindsWithOptionalIntegrations).toEqual(new Set(["calendar"])); expect(widgetKindsWithOptionalIntegrations).toEqual(new Set(["calendar"]));
expect(widgetIntegrationLimits).toEqual({ expect(widgetIntegrationLimits).toEqual({
@@ -41,32 +41,32 @@ export const nativeFeatureCapabilities = {
(kind) => kind !== "patchmon" && kind !== "wud", (kind) => kind !== "patchmon" && kind !== "wud",
), ),
}, },
firewall: { integrations: getIntegrationKindsByCategory("firewall") }, firewall: { integrations: [...getIntegrationKindsByCategory("firewall"), "mock"] },
notifications: { integrations: getIntegrationKindsByCategory("notifications") }, notifications: { integrations: getIntegrationKindsByCategory("notifications") },
mediaReleases: { integrations: ["mock", "emby", "jellyfin", "plex"] }, mediaReleases: { integrations: ["mock", "emby", "jellyfin", "plex"] },
systemResources: { integrations: ["dashDot", "openmediavault", "truenas", "unraid", "glances", "synology"] }, systemResources: { integrations: ["dashDot", "openmediavault", "truenas", "unraid", "glances", "synology", "mock"] },
systemDisks: { integrations: ["dashDot", "openmediavault", "truenas", "unraid", "synology"] }, systemDisks: { integrations: ["dashDot", "openmediavault", "truenas", "unraid", "synology", "mock"] },
beszelAlerts: { integrations: ["beszel", "mock"] }, beszelAlerts: { integrations: ["beszel", "mock"] },
beszelSystemGrid: { integrations: ["beszel", "mock"] }, beszelSystemGrid: { integrations: ["beszel", "mock"] },
beszelSystemStats: { integrations: ["beszel", "mock"] }, beszelSystemStats: { integrations: ["beszel", "mock"] },
beszelSystemTable: { integrations: ["beszel", "mock"] }, beszelSystemTable: { integrations: ["beszel", "mock"] },
coolify: { integrations: ["coolify"] }, coolify: { integrations: ["coolify", "mock"] },
"immich-serverStats": { integrations: ["immich"], serverMaxIntegrations: 1 }, "immich-serverStats": { integrations: ["immich", "mock"], serverMaxIntegrations: 1 },
"immich-albumCarousel": { integrations: ["immich"], serverMaxIntegrations: 1 }, "immich-albumCarousel": { integrations: ["immich", "mock"], serverMaxIntegrations: 1 },
paperlessNgx: { integrations: ["paperlessNgx"], serverMaxIntegrations: 1 }, paperlessNgx: { integrations: ["paperlessNgx", "mock"], serverMaxIntegrations: 1 },
patchmon: { integrations: ["patchmon"], serverMaxIntegrations: 1 }, patchmon: { integrations: ["patchmon", "mock"], serverMaxIntegrations: 1 },
bazarr: { integrations: ["bazarr"], serverMaxIntegrations: 1 }, bazarr: { integrations: ["bazarr", "mock"], serverMaxIntegrations: 1 },
tracearr: { integrations: ["tracearr"] }, tracearr: { integrations: ["tracearr", "mock"] },
speedtestTracker: { integrations: ["speedtestTracker"] }, speedtestTracker: { integrations: ["speedtestTracker", "mock"] },
uptimeKuma: { integrations: ["uptimeKuma"] }, uptimeKuma: { integrations: ["uptimeKuma", "mock"] },
audioStats: { integrations: ["navidrome", "audiobookshelf"], serverMaxIntegrations: 1 }, audioStats: { integrations: ["navidrome", "audiobookshelf", "mock"], serverMaxIntegrations: 1 },
umami: { integrations: ["umami"], serverMaxIntegrations: 1 }, umami: { integrations: ["umami", "mock"], serverMaxIntegrations: 1 },
vpn: { integrations: getIntegrationKindsByCategory("vpn") }, vpn: { integrations: [...getIntegrationKindsByCategory("vpn"), "mock"] },
ups: { integrations: getIntegrationKindsByCategory("ups") }, ups: { integrations: getIntegrationKindsByCategory("ups") },
archiveTeamWarrior: { integrations: ["archiveTeamWarrior"], serverMaxIntegrations: 1 }, archiveTeamWarrior: { integrations: ["archiveTeamWarrior", "mock"], serverMaxIntegrations: 1 },
anchorNote: { integrations: ["anchor"], serverMaxIntegrations: 1 }, anchorNote: { integrations: ["anchor", "mock"], serverMaxIntegrations: 1 },
traefik: { integrations: ["traefik"] }, traefik: { integrations: ["traefik", "mock"] },
wud: { integrations: ["wud"], serverMaxIntegrations: 1 }, wud: { integrations: ["wud", "mock"], serverMaxIntegrations: 1 },
} as const satisfies Partial<Record<WidgetKind, NativeFeatureCapabilityDescriptor>>; } as const satisfies Partial<Record<WidgetKind, NativeFeatureCapabilityDescriptor>>;
const nativeFeatureCapabilityEntries = Object.entries(nativeFeatureCapabilities) as [ const nativeFeatureCapabilityEntries = Object.entries(nativeFeatureCapabilities) as [
+1
View File
@@ -45,6 +45,7 @@ export { UmamiIntegration } from "./umami/umami-integration";
export { PeaNutIntegration } from "./peanut/peanut-integration"; export { PeaNutIntegration } from "./peanut/peanut-integration";
export { BazarrIntegration } from "./bazarr/bazarr-integration"; export { BazarrIntegration } from "./bazarr/bazarr-integration";
export { TraefikIntegration } from "./traefik/traefik-integration"; export { TraefikIntegration } from "./traefik/traefik-integration";
export { mockWidgetData } from "./mock/widget-data";
// Types // Types
export type { IntegrationInput } from "./base/integration"; export type { IntegrationInput } from "./base/integration";
@@ -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;
+1 -1
View File
@@ -8,7 +8,7 @@ export const { definition, componentLoader } = createWidgetDefinition("anchorNot
supportsAdvancedFocus: true, supportsAdvancedFocus: true,
queryKey: [["widget", "anchorNotes"]], queryKey: [["widget", "anchorNotes"]],
maxIntegrations: 1, maxIntegrations: 1,
supportedIntegrations: ["anchor"], supportedIntegrations: ["anchor", "mock"],
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
noteId: factory.anchorNote(), noteId: factory.anchorNote(),
@@ -14,5 +14,5 @@ export const { definition, componentLoader } = createWidgetDefinition("archiveTe
}), }),
})); }));
}, },
supportedIntegrations: ["archiveTeamWarrior"], supportedIntegrations: ["archiveTeamWarrior", "mock"],
}).withDynamicImport(() => import("./component")); }).withDynamicImport(() => import("./component"));
+3 -2
View File
@@ -4,7 +4,8 @@ import { createWidgetDefinition, matchesWidgetRuntimeQuery } from "../definition
import { optionsBuilder } from "../options"; import { optionsBuilder } from "../options";
const hideUnlessNavidrome = { const hideUnlessNavidrome = {
shouldHide: (_: unknown, integrationKinds: string[]) => !integrationKinds.includes("navidrome"), shouldHide: (_: unknown, integrationKinds: string[]) =>
!integrationKinds.includes("navidrome") && !integrationKinds.includes("mock"),
}; };
const hideUnlessAudiobookshelf = { const hideUnlessAudiobookshelf = {
@@ -16,7 +17,7 @@ export const { definition, componentLoader } = createWidgetDefinition("audioStat
supportsAdvancedFocus: true, supportsAdvancedFocus: true,
queryKeys: [[["widget", "audioStats", "getStats"]], [["widget", "mediaServer", "getCurrentStreams"]]], queryKeys: [[["widget", "audioStats", "getStats"]], [["widget", "mediaServer", "getCurrentStreams"]]],
queryMatcher: matchesWidgetRuntimeQuery, queryMatcher: matchesWidgetRuntimeQuery,
supportedIntegrations: ["navidrome", "audiobookshelf"], supportedIntegrations: ["navidrome", "audiobookshelf", "mock"],
integrationsRequired: true, integrationsRequired: true,
maxIntegrations: 1, maxIntegrations: 1,
createOptions() { createOptions() {
+1 -1
View File
@@ -6,7 +6,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("bazarr", { export const { definition, componentLoader } = createWidgetDefinition("bazarr", {
supportsAdvancedFocus: true, supportsAdvancedFocus: true,
icon: IconBadgeCc, icon: IconBadgeCc,
supportedIntegrations: ["bazarr"], supportedIntegrations: ["bazarr", "mock"],
integrationsRequired: true, integrationsRequired: true,
maxIntegrations: 1, maxIntegrations: 1,
createOptions() { createOptions() {
+1 -1
View File
@@ -19,7 +19,7 @@ export const { definition, componentLoader } = createWidgetDefinition("coolify",
}), }),
})); }));
}, },
supportedIntegrations: ["coolify"], supportedIntegrations: ["coolify", "mock"],
errors: { errors: {
INTERNAL_SERVER_ERROR: { INTERNAL_SERVER_ERROR: {
icon: IconServerOff, icon: IconServerOff,
+1 -1
View File
@@ -12,7 +12,7 @@ export const { definition, componentLoader } = createWidgetDefinition("firewall"
createOptions() { createOptions() {
return optionsBuilder.from(() => ({})); return optionsBuilder.from(() => ({}));
}, },
supportedIntegrations: getIntegrationKindsByCategory("firewall"), supportedIntegrations: [...getIntegrationKindsByCategory("firewall"), "mock"],
errors: { errors: {
INTERNAL_SERVER_ERROR: { INTERNAL_SERVER_ERROR: {
icon: IconWallOff, icon: IconWallOff,
@@ -62,7 +62,7 @@ export const { definition, componentLoader } = createWidgetDefinition("immich-al
: undefined, : undefined,
}), }),
refetchInterval: null, refetchInterval: null,
supportedIntegrations: ["immich"], supportedIntegrations: ["immich", "mock"],
integrationsRequired: true, integrationsRequired: true,
maxIntegrations: 1, maxIntegrations: 1,
contextActions: ({ widgetRuntimeRef }) => { contextActions: ({ widgetRuntimeRef }) => {
@@ -9,7 +9,7 @@ export const { definition, componentLoader } = createWidgetDefinition("immich-se
queryKeys: [[["widget", "immich", "getServerStats"]], [["widget", "immich", "getAlbums"]]], queryKeys: [[["widget", "immich", "getServerStats"]], [["widget", "immich", "getAlbums"]]],
queryMatcher: matchesWidgetRuntimeQuery, queryMatcher: matchesWidgetRuntimeQuery,
refetchInterval: null, refetchInterval: null,
supportedIntegrations: ["immich"], supportedIntegrations: ["immich", "mock"],
integrationsRequired: true, integrationsRequired: true,
maxIntegrations: 1, maxIntegrations: 1,
createOptions() { createOptions() {
+1 -1
View File
@@ -7,7 +7,7 @@ export const { definition, componentLoader } = createWidgetDefinition("paperless
supportsAdvancedFocus: false, supportsAdvancedFocus: false,
icon: IconFileText, icon: IconFileText,
refetchInterval: null, refetchInterval: null,
supportedIntegrations: ["paperlessNgx"], supportedIntegrations: ["paperlessNgx", "mock"],
integrationsRequired: true, integrationsRequired: true,
maxIntegrations: 1, maxIntegrations: 1,
createOptions() { createOptions() {
+1 -1
View File
@@ -9,7 +9,7 @@ import { patchmonOptionsSuperRefine } from "./threshold-validation";
export const { definition, componentLoader } = createWidgetDefinition("patchmon", { export const { definition, componentLoader } = createWidgetDefinition("patchmon", {
supportsAdvancedFocus: true, supportsAdvancedFocus: true,
icon: IconShieldCheck, icon: IconShieldCheck,
supportedIntegrations: ["patchmon"], supportedIntegrations: ["patchmon", "mock"],
integrationsRequired: true, integrationsRequired: true,
maxIntegrations: 1, maxIntegrations: 1,
createOptions() { createOptions() {
@@ -30,7 +30,7 @@ export const { definition, componentLoader } = createWidgetDefinition("speedtest
}, },
); );
}, },
supportedIntegrations: ["speedtestTracker"], supportedIntegrations: ["speedtestTracker", "mock"],
errors: { errors: {
INTERNAL_SERVER_ERROR: { INTERNAL_SERVER_ERROR: {
icon: IconServerOff, icon: IconServerOff,
+1 -1
View File
@@ -9,7 +9,7 @@ export const { definition, componentLoader } = createWidgetDefinition("systemDis
icon: IconServer2, icon: IconServer2,
queryKey: [["widget", "healthMonitoring"]], queryKey: [["widget", "healthMonitoring"]],
refetchInterval: 10, refetchInterval: 10,
supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid", "synology"], supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid", "synology", "mock"],
createOptions() { createOptions() {
return optionsBuilder.from( return optionsBuilder.from(
(factory) => ({ (factory) => ({
@@ -24,7 +24,7 @@ export const { definition, componentLoader } = createWidgetDefinition("systemRes
supportsAdvancedFocus: true, supportsAdvancedFocus: true,
queryKey: [["widget", "healthMonitoring"]], queryKey: [["widget", "healthMonitoring"]],
refetchInterval: 10, refetchInterval: 10,
supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid", "glances", "synology"], supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid", "glances", "synology", "mock"],
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
hasShadow: factory.switch({ defaultValue: true }), hasShadow: factory.switch({ defaultValue: true }),
+1 -1
View File
@@ -24,7 +24,7 @@ export const { definition, componentLoader } = createWidgetDefinition("tracearr"
}), }),
})); }));
}, },
supportedIntegrations: ["tracearr"], supportedIntegrations: ["tracearr", "mock"],
errors: { errors: {
INTERNAL_SERVER_ERROR: { INTERNAL_SERVER_ERROR: {
icon: IconServerOff, icon: IconServerOff,
+1 -1
View File
@@ -19,7 +19,7 @@ export const { definition, componentLoader } = createWidgetDefinition("traefik",
}), }),
})); }));
}, },
supportedIntegrations: ["traefik"], supportedIntegrations: ["traefik", "mock"],
errors: { errors: {
INTERNAL_SERVER_ERROR: { INTERNAL_SERVER_ERROR: {
icon: IconRouteOff, icon: IconRouteOff,
+1 -1
View File
@@ -18,7 +18,7 @@ export const { definition, componentLoader } = createWidgetDefinition("umami", {
], ],
queryMatcher: matchesWidgetRuntimeQuery, queryMatcher: matchesWidgetRuntimeQuery,
refetchInterval: null, refetchInterval: null,
supportedIntegrations: ["umami"], supportedIntegrations: ["umami", "mock"],
integrationsRequired: true, integrationsRequired: true,
maxIntegrations: 1, maxIntegrations: 1,
createOptions() { createOptions() {
+1 -1
View File
@@ -34,7 +34,7 @@ export const { definition, componentLoader } = createWidgetDefinition("uptimeKum
}), }),
})); }));
}, },
supportedIntegrations: ["uptimeKuma"], supportedIntegrations: ["uptimeKuma", "mock"],
errors: { errors: {
INTERNAL_SERVER_ERROR: { INTERNAL_SERVER_ERROR: {
icon: IconServerOff, icon: IconServerOff,
+1 -1
View File
@@ -12,5 +12,5 @@ export const { componentLoader, definition } = createWidgetDefinition("vpn", {
createOptions() { createOptions() {
return optionsBuilder.from(() => ({})); return optionsBuilder.from(() => ({}));
}, },
supportedIntegrations: getIntegrationKindsByCategory("vpn"), supportedIntegrations: [...getIntegrationKindsByCategory("vpn"), "mock"],
}).withDynamicImport(() => import("./component")); }).withDynamicImport(() => import("./component"));
+1 -1
View File
@@ -7,7 +7,7 @@ export const { definition, componentLoader } = createWidgetDefinition("wud", {
icon: IconBrandDocker, icon: IconBrandDocker,
supportsAdvancedFocus: true, supportsAdvancedFocus: true,
refetchInterval: null, refetchInterval: null,
supportedIntegrations: ["wud"], supportedIntegrations: ["wud", "mock"],
integrationsRequired: true, integrationsRequired: true,
maxIntegrations: 1, maxIntegrations: 1,
createOptions() { createOptions() {