From 0a6467979a5e3b3e3eae6252c31fca1d9af54dc1 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 5 Jun 2026 16:36:45 +0200 Subject: [PATCH] feat: add comprehensive demo mode with mock integrations (#5870) --- .../app/[locale]/_client-providers/trpc.tsx | 17 ++ .../src/app/[locale]/auth/login/page.tsx | 12 +- .../boards/(content)/_header-actions.tsx | 27 +- apps/nextjs/src/app/[locale]/layout.tsx | 1 + .../app/[locale]/manage/integrations/page.tsx | 2 +- apps/nextjs/src/env.ts | 1 + docs/README.md | 3 + packages/api/src/env.ts | 4 +- packages/api/src/middlewares/docker.ts | 4 +- packages/api/src/router/info.ts | 3 +- packages/api/src/trpc.ts | 24 +- packages/db/migrations/seed.ts | 208 +++++++++++++- .../integrations/src/mock/data/calendar.ts | 260 +++++++++++++----- .../integrations/src/mock/data/download.ts | 164 +++++++++-- .../src/mock/data/media-releases.ts | 121 ++++---- .../src/mock/data/media-server.ts | 119 ++++++-- .../mock/data/network-controller-summary.ts | 26 +- .../integrations/src/mock/mock-integration.ts | 4 +- packages/notifications/src/index.tsx | 9 +- packages/request-handler/src/docker.ts | 159 ++++++++++- 20 files changed, 932 insertions(+), 236 deletions(-) diff --git a/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx b/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx index 0e36cb4a1..7c7ca0766 100644 --- a/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx +++ b/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx @@ -17,10 +17,13 @@ import { import superjson from "superjson"; import type { SuperJSONResult } from "superjson"; +import { TRPCClientError } from "@trpc/client"; + import type { AppRouter } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; import { createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/shared"; import { env } from "@homarr/common/env"; +import { showWarningNotification } from "@homarr/notifications"; const getWebSocketProtocol = () => { // window is not defined on server side @@ -56,6 +59,20 @@ export function TRPCReactProvider(props: PropsWithChildren) { queries: { staleTime: 5 * 1000, }, + mutations: { + onError(error) { + if ( + error instanceof TRPCClientError && + error.data?.code === "FORBIDDEN" && + error.message === "Mutations are disabled in demo mode" + ) { + showWarningNotification({ + title: "Demo mode", + message: "This action is disabled in demo mode.", + }); + } + }, + }, }, }), ); diff --git a/apps/nextjs/src/app/[locale]/auth/login/page.tsx b/apps/nextjs/src/app/[locale]/auth/login/page.tsx index 6cf00b6b3..30eb88ba0 100644 --- a/apps/nextjs/src/app/[locale]/auth/login/page.tsx +++ b/apps/nextjs/src/app/[locale]/auth/login/page.tsx @@ -1,11 +1,14 @@ import { redirect } from "next/navigation"; -import { Card, Center, Stack, Text, Title } from "@mantine/core"; +import { Alert, Card, Center, Code, Stack, Text, Title } from "@mantine/core"; +import { IconLogin } from "@tabler/icons-react"; import { env } from "@homarr/auth/env"; import { auth } from "@homarr/auth/next"; import { getScopedI18n } from "@homarr/translation/server"; import { sanitizeRedirectionUrl } from "@homarr/validation/redirection-url"; +import { env as appEnv } from "~/env"; + import { HomarrLogoWithTitle } from "~/components/layout/logo/homarr-logo"; import { LoginForm } from "./_login-form"; @@ -37,6 +40,13 @@ export default async function Login(props: LoginProps) { {t("subtitle")} + {appEnv.DEMO_MODE && ( + } color="blue" variant="light" w={64 * 6} maw="90vw"> + + Demo mode is enabled. Sign in with username demo and password demo + + + )} { const [isEditMode] = useEditMode(); const board = useRequiredBoard(); const { hasChangeAccess } = useBoardPermissions(board); + const { data: demoMode, isLoading } = clientApi.info.isDemoMode.useQuery(); - if (!hasChangeAccess) { + if (!hasChangeAccess || isLoading) { return ; } @@ -51,11 +52,13 @@ export const BoardContentHeaderActions = () => { <> {isEditMode && } - + - - - + {!demoMode && ( + + + + )} @@ -141,7 +144,7 @@ const AddMenu = () => { ); }; -const EditModeMenu = () => { +const EditModeMenu = ({ demoMode }: { demoMode: boolean }) => { const [isEditMode, { open, close }] = useEditMode(); const board = useRequiredBoard(); const utils = clientApi.useUtils(); @@ -164,10 +167,18 @@ const EditModeMenu = () => { }, }); + const discardDemoChanges = useCallback(() => { + void utils.board.getBoardByName.invalidate({ name: board.name }); + close(); + }, [utils, board.name, close]); + const toggle = useCallback(() => { - if (isEditMode) return saveBoard(board); + if (isEditMode) { + if (demoMode) return discardDemoChanges(); + return saveBoard(board); + } open(); - }, [board, isEditMode, saveBoard, open]); + }, [board, isEditMode, demoMode, saveBoard, open, discardDemoChanges]); useHotkeys([[hotkeys.toggleBoardEdit, toggle]]); usePreventLeaveWithDirty(isEditMode); diff --git a/apps/nextjs/src/app/[locale]/layout.tsx b/apps/nextjs/src/app/[locale]/layout.tsx index e88f1965b..ac7d8d856 100644 --- a/apps/nextjs/src/app/[locale]/layout.tsx +++ b/apps/nextjs/src/app/[locale]/layout.tsx @@ -25,6 +25,7 @@ import { isLocaleRTL, isLocaleSupported } from "@homarr/translation"; import { Analytics } from "~/components/layout/analytics"; import { CrowdinLiveTranslation } from "~/components/layout/crowdin-live-translation"; + import { SearchEngineOptimization } from "~/components/layout/search-engine-optimization"; import { ServiceWorkerRegistration } from "~/components/layout/service-worker-registration"; import { getCurrentColorSchemeAsync } from "~/theme/color-scheme"; diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/page.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/page.tsx index 994d63b3c..830345203 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/integrations/page.tsx @@ -96,7 +96,7 @@ const IntegrationSelectMenu = ({ children }: PropsWithChildren) => { > {children} - + ); diff --git a/apps/nextjs/src/env.ts b/apps/nextjs/src/env.ts index 62b49dee8..96f03e7b7 100644 --- a/apps/nextjs/src/env.ts +++ b/apps/nextjs/src/env.ts @@ -3,6 +3,7 @@ import { createBooleanSchema, createEnv } from "@homarr/core/infrastructure/env" export const env = createEnv({ server: { UNSAFE_ENABLE_MOCK_INTEGRATION: createBooleanSchema(false), + DEMO_MODE: createBooleanSchema(false), }, experimental__runtimeEnv: process.env, }); diff --git a/docs/README.md b/docs/README.md index e7aeef585..9809953bf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,6 +19,9 @@

+ + Live Demo 🎮 + • Install 💻 • diff --git a/packages/api/src/env.ts b/packages/api/src/env.ts index 7cc81cc57..ba26a264a 100644 --- a/packages/api/src/env.ts +++ b/packages/api/src/env.ts @@ -1,12 +1,14 @@ import { z } from "zod/v4"; -import { createEnv } from "@homarr/core/infrastructure/env"; +import { createBooleanSchema, createEnv } from "@homarr/core/infrastructure/env"; export const env = createEnv({ server: { KUBERNETES_SERVICE_ACCOUNT_NAME: z.string().optional(), + DEMO_MODE: createBooleanSchema(false), }, runtimeEnv: { KUBERNETES_SERVICE_ACCOUNT_NAME: process.env.KUBERNETES_SERVICE_ACCOUNT_NAME, + DEMO_MODE: process.env.DEMO_MODE, }, }); diff --git a/packages/api/src/middlewares/docker.ts b/packages/api/src/middlewares/docker.ts index 5a317b95a..e4f04f192 100644 --- a/packages/api/src/middlewares/docker.ts +++ b/packages/api/src/middlewares/docker.ts @@ -2,11 +2,11 @@ import { TRPCError } from "@trpc/server"; import { env } from "@homarr/docker/env"; -import { publicProcedure } from "../trpc"; +import { isDemoMode, publicProcedure } from "../trpc"; export const dockerMiddleware = () => { return publicProcedure.use(async ({ next }) => { - if (env.ENABLE_DOCKER) { + if (env.ENABLE_DOCKER || isDemoMode) { return await next(); } throw new TRPCError({ diff --git a/packages/api/src/router/info.ts b/packages/api/src/router/info.ts index 3c205963c..8befca412 100644 --- a/packages/api/src/router/info.ts +++ b/packages/api/src/router/info.ts @@ -1,7 +1,7 @@ import z from "zod/v4"; import packageJson from "../../../../package.json"; -import { createTRPCRouter, protectedProcedure } from "../trpc"; +import { createTRPCRouter, isDemoMode, publicProcedure, protectedProcedure } from "../trpc"; export const infoRouter = createTRPCRouter({ getInfo: protectedProcedure @@ -13,4 +13,5 @@ export const infoRouter = createTRPCRouter({ version: packageJson.version, }; }), + isDemoMode: publicProcedure.query(() => isDemoMode), }); diff --git a/packages/api/src/trpc.ts b/packages/api/src/trpc.ts index bedd60f3d..eb4da06c2 100644 --- a/packages/api/src/trpc.ts +++ b/packages/api/src/trpc.ts @@ -18,6 +18,7 @@ import { createLogger } from "@homarr/core/infrastructure/logs"; import { db } from "@homarr/db"; import type { GroupPermissionKey, OnboardingStep } from "@homarr/definitions"; +import { env } from "./env"; import { getOnboardingOrFallbackAsync } from "./router/onboard/onboard-queries"; const logger = createLogger({ module: "trpc" }); @@ -87,6 +88,20 @@ export const createCallerFactory = t.createCallerFactory; */ export const createTRPCRouter = t.router; +export const isDemoMode = env.DEMO_MODE; + +const enforceDemoModeReadOnly = t.middleware(({ ctx, next, type }) => { + if (env.DEMO_MODE && type === "mutation") { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Mutations are disabled in demo mode", + }); + } + return next({ ctx }); +}); + +const baseProcedure = env.DEMO_MODE ? t.procedure.use(enforceDemoModeReadOnly) : t.procedure; + /** * Public (unauthed) procedure * @@ -94,19 +109,14 @@ export const createTRPCRouter = t.router; * tRPC API. It does not guarantee that a user querying is authorized, but you * can still access user session data if they are logged in */ -export const publicProcedure = t.procedure; +export const publicProcedure = baseProcedure; -/** - * Reusable middleware that enforces users are logged in before running the - * procedure - */ const enforceUserIsAuthed = t.middleware(({ ctx, next }) => { if (!ctx.session?.user) { throw new TRPCError({ code: "UNAUTHORIZED" }); } return next({ ctx: { - // infers the `session` as non-nullable session: { ...ctx.session, user: ctx.session.user }, }, }); @@ -121,7 +131,7 @@ const enforceUserIsAuthed = t.middleware(({ ctx, next }) => { * * @see https://trpc.io/docs/procedures */ -export const protectedProcedure = t.procedure.use(enforceUserIsAuthed); +export const protectedProcedure = baseProcedure.use(enforceUserIsAuthed); /** * Procedure that requires a specific permission diff --git a/packages/db/migrations/seed.ts b/packages/db/migrations/seed.ts index a2dec94af..0a00e9674 100644 --- a/packages/db/migrations/seed.ts +++ b/packages/db/migrations/seed.ts @@ -1,3 +1,5 @@ +import SuperJSON from "superjson"; + import { createId, objectKeys } from "@homarr/common"; import { createDocumentationLink, @@ -8,6 +10,7 @@ import { integrationDefs, integrationKinds, } from "@homarr/definitions"; +import type { WidgetKind } from "@homarr/definitions"; import { defaultServerSettings, defaultServerSettingsKeys } from "@homarr/server-settings"; import type { Database } from ".."; @@ -18,12 +21,17 @@ import { insertServerSettingByKeyAsync, updateServerSettingByKeyAsync, } from "../queries/server-setting"; + import { + apps, boards, groupMembers, groupPermissions, groups, + integrationItems, integrations, + itemLayouts, + items, layouts, onboarding, searchEngines, @@ -39,7 +47,7 @@ export const seedDataAsync = async (db: Database) => { await seedDefaultSearchEnginesAsync(db); await seedDefaultIntegrationsAsync(db); - if (process.env.DEMO_MODE === "true") { + if (["1", "yes", "t", "true"].includes((process.env.DEMO_MODE ?? "").toLowerCase())) { await seedDemoUserAsync(db); } }; @@ -209,6 +217,134 @@ const seedDefaultIntegrationsAsync = async (db: Database) => { console.log(`Created ${createdCount} default integrations through seeding process`); }; +interface DemoWidget { + kind: WidgetKind; + width: number; + height: number; + needsIntegration: boolean; + options?: Record; +} + +const demoApps = [ + { + name: "Sonarr", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/sonarr.svg", + href: "https://sonarr.tv", + }, + { + name: "Radarr", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/radarr.svg", + href: "https://radarr.video", + }, + { + name: "Plex", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/plex.svg", + href: "https://plex.tv", + }, + { + name: "Jellyfin", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/jellyfin.svg", + href: "https://jellyfin.org", + }, + { + name: "Proxmox", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/proxmox.svg", + href: "https://proxmox.com", + }, + { + name: "Pi-hole", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/pi-hole.svg", + href: "https://pi-hole.net", + }, + { + name: "Grafana", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/grafana.svg", + href: "https://grafana.com", + }, + { + name: "Portainer", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/portainer.svg", + href: "https://portainer.io", + }, + { + name: "Home Assistant", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/home-assistant.svg", + href: "https://home-assistant.io", + }, + { + name: "Nextcloud", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/nextcloud.svg", + href: "https://nextcloud.com", + }, + { + name: "qBittorrent", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/qbittorrent.svg", + href: "https://qbittorrent.org", + }, + { + name: "Overseerr", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/overseerr.svg", + href: "https://overseerr.dev", + }, +] as const; + +const buildDemoWidgets = (appIds: string[]): DemoWidget[] => [ + // Row 1: calendar + downloads + clock = 12 + { kind: "calendar", width: 5, height: 3, needsIntegration: true }, + { kind: "downloads", width: 5, height: 3, needsIntegration: true }, + { kind: "clock", width: 2, height: 1, needsIntegration: false }, + // Row 2: healthMonitoring + bookmarks = 12 + { kind: "healthMonitoring", width: 6, height: 3, needsIntegration: true }, + { + kind: "bookmarks", + width: 6, + height: 3, + needsIntegration: false, + options: { title: "Homelab", items: appIds, layout: "grid", openNewTab: true }, + }, + // Row 3: mediaServer + mediaTranscoding + dnsHoleSummary = 12 + { kind: "mediaServer", width: 3, height: 2, needsIntegration: true }, + { kind: "mediaTranscoding", width: 3, height: 2, needsIntegration: true }, + { kind: "dnsHoleSummary", width: 6, height: 2, needsIntegration: true }, + // Row 4: dnsHoleControls + mediaReleases + notifications = 12 + { kind: "dnsHoleControls", width: 2, height: 1, needsIntegration: true }, + { kind: "mediaReleases", width: 4, height: 2, needsIntegration: true }, + { kind: "notifications", width: 6, height: 2, needsIntegration: true }, + // Row 5: requestList + requestStats + indexerManager = 12 + { kind: "mediaRequests-requestList", width: 4, height: 3, needsIntegration: true }, + { kind: "mediaRequests-requestStats", width: 4, height: 2, needsIntegration: true }, + { kind: "indexerManager", width: 4, height: 2, needsIntegration: true }, + // Row 6: networkControllerSummary + networkControllerStatus + rssFeed = 12 + { kind: "networkControllerSummary", width: 3, height: 2, needsIntegration: true }, + { kind: "networkControllerStatus", width: 3, height: 2, needsIntegration: true }, + { + kind: "rssFeed", + width: 6, + height: 2, + needsIntegration: false, + options: { + feedUrls: ["https://selfh.st/rss/", "https://hnrss.org/newest?q=self-hosted"], + maximumAmountPosts: 20, + textLinesClamp: 2, + hideDescription: false, + }, + }, + // Row 7-8: app widgets (2x1 each, 6 per row = 12) + ...appIds.map( + (appId): DemoWidget => ({ + kind: "app", + width: 2, + height: 1, + needsIntegration: false, + options: { appId, openInNewTab: true, showTitle: true, pingEnabled: false }, + }), + ), + // Row 9: notebook + dockerContainers + weather = 12 + { kind: "notebook", width: 4, height: 4, needsIntegration: false }, + { kind: "dockerContainers", width: 6, height: 2, needsIntegration: false }, + { kind: "weather", width: 2, height: 1, needsIntegration: false }, +]; + const seedDemoUserAsync = async (db: Database) => { const existingUser = await db.query.users.findFirst({ where: eq(users.name, "demo"), @@ -225,7 +361,7 @@ const seedDemoUserAsync = async (db: Database) => { id: userId, name: "demo", email: "demo@example.com", - password: "$2b$10$c5AaQb0oj32/bui5Z55PyOb7pY.iEkXYqgdD8MjS9LjSjLMeOkZu2", + password: "$2b$10$odRXt5e95kSQV5Axmk/FeO6GVOxuRQQ8NnRcBA78Wg4V3kZxPY68u", }); const maxPosition = await getMaxGroupPositionAsync(db); @@ -250,6 +386,27 @@ const seedDemoUserAsync = async (db: Database) => { previousStep: "settings", }); + const integrationId = createId(); + await db.insert(integrations).values({ + id: integrationId, + name: "Demo Integration", + url: "https://demo.homarr.dev", + kind: "mock", + appId: null, + }); + + const appIds: string[] = []; + for (const app of demoApps) { + const appId = createId(); + appIds.push(appId); + await db.insert(apps).values({ + id: appId, + name: app.name, + iconUrl: app.iconUrl, + href: app.href, + }); + } + const boardId = createId(); await db.insert(boards).values({ id: boardId, @@ -257,23 +414,64 @@ const seedDemoUserAsync = async (db: Database) => { isPublic: false, creatorId: userId, }); + + const sectionId = createId(); await db.insert(sections).values({ - id: createId(), + id: sectionId, kind: "empty", xOffset: 0, yOffset: 0, boardId, }); + + const layoutId = createId(); await db.insert(layouts).values({ - id: createId(), + id: layoutId, name: "Base", columnCount: 12, breakpoint: 0, boardId, }); + + const demoWidgets = buildDemoWidgets(appIds); + let xOffset = 0; + let yOffset = 0; + let rowMaxHeight = 0; + for (const widget of demoWidgets) { + if (xOffset + widget.width > 12) { + xOffset = 0; + yOffset += rowMaxHeight; + rowMaxHeight = 0; + } + rowMaxHeight = Math.max(rowMaxHeight, widget.height); + const itemId = createId(); + await db.insert(items).values({ + id: itemId, + boardId, + kind: widget.kind, + ...(widget.options ? { options: SuperJSON.stringify(widget.options) } : {}), + }); + await db.insert(itemLayouts).values({ + itemId, + sectionId, + layoutId, + xOffset, + yOffset, + width: widget.width, + height: widget.height, + }); + if (widget.needsIntegration) { + await db.insert(integrationItems).values({ + itemId, + integrationId, + }); + } + xOffset += widget.width; + } + await db.update(users).set({ homeBoardId: boardId }).where(eq(users.id, userId)); console.log( - "Your instance has enabled demo mode. Homarr therefore created example data for your demo. You can disable demo mode anytime by setting the env 'DEMO_MODE' to 'false' or unset it.", + "Demo mode enabled: created demo user, mock integration, and sample board with widgets. Disable by setting DEMO_MODE=false.", ); }; diff --git a/packages/integrations/src/mock/data/calendar.ts b/packages/integrations/src/mock/data/calendar.ts index 530819dd3..4ebd36d6e 100644 --- a/packages/integrations/src/mock/data/calendar.ts +++ b/packages/integrations/src/mock/data/calendar.ts @@ -1,24 +1,130 @@ import type { ICalendarIntegration } from "../../interfaces/calendar/calendar-integration"; import type { CalendarEvent } from "../../interfaces/calendar/calendar-types"; -export class CalendarMockService implements ICalendarIntegration { - public async getCalendarEventsAsync(start: Date, end: Date, _includeUnmonitored: boolean): Promise { - const result = [homarrMeetup(start, end), titanicRelease(start, end), seriesRelease(start, end)]; - return await Promise.resolve(result); - } +interface EventTemplate { + title: string; + subTitle: string; + description: string; + dayOffset: number; + hours: number; + image: CalendarEvent["image"]; + indicatorColor: string; + metadata?: CalendarEvent["metadata"]; + location?: string; + links: CalendarEvent["links"]; } -const homarrMeetup = (start: Date, end: Date): CalendarEvent => { - const startDate = randomDateBetween(start, end); - const endDate = new Date(startDate.getTime() + 2 * 60 * 60 * 1000); // 2 hours later - return { +const eventTemplates: EventTemplate[] = [ + { + title: "The Mandalorian", + subTitle: "Season 4", + description: "A lone bounty hunter in the outer reaches of the galaxy.", + dayOffset: 0, + hours: 20, + image: { + src: "https://image.tmdb.org/t/p/w300/sWgBv7LV2PRoQgkxwlibdGXKz1S.jpg", + aspectRatio: { width: 7, height: 12 }, + badge: { content: "S4/E3", color: "red" }, + }, + indicatorColor: "blue", + links: [ + { + href: "https://www.imdb.com/title/tt8111088/", + name: "IMDb", + color: "#f5c518", + isDark: false, + logo: "/images/apps/imdb.svg", + }, + ], + }, + { + title: "Silo", + subTitle: "Season 2", + description: "In a ruined and toxic future, thousands live in a giant underground silo.", + dayOffset: 3, + hours: 21, + image: { + src: "https://image.tmdb.org/t/p/w300/2xSzAuKMhERTcPA19SpFQtVrMC0.jpg", + aspectRatio: { width: 7, height: 12 }, + badge: { content: "S2/E8", color: "teal" }, + }, + indicatorColor: "teal", + links: [ + { + href: "https://www.imdb.com/title/tt14688458/", + name: "IMDb", + color: "#f5c518", + isDark: false, + logo: "/images/apps/imdb.svg", + }, + ], + }, + { + title: "Dune: Part Two", + subTitle: "Digital Release", + description: "Paul Atreides unites with the Fremen to seek revenge against the conspirators.", + dayOffset: 5, + hours: 0, + image: { + src: "https://image.tmdb.org/t/p/w300/8b8R8l88Qje9dn9OE8PY05Nez7.jpg", + aspectRatio: { width: 7, height: 12 }, + }, + metadata: { type: "radarr", releaseType: "digitalRelease" }, + indicatorColor: "cyan", + links: [ + { + href: "https://www.imdb.com/title/tt15239678/", + name: "IMDb", + color: "#f5c518", + isDark: false, + logo: "/images/apps/imdb.svg", + }, + ], + }, + { + title: "Severance", + subTitle: "Season 2", + description: "Mark leads a team of office workers whose memories are surgically divided.", + dayOffset: 7, + hours: 22, + image: { + src: "https://image.tmdb.org/t/p/w300/pJAtR4gMFnalHCmT9cByPFgkZ6B.jpg", + aspectRatio: { width: 7, height: 12 }, + badge: { content: "S2/E5", color: "violet" }, + }, + indicatorColor: "violet", + links: [], + }, + { + title: "Oppenheimer", + subTitle: "Physical Release", + description: "The story of the development of the atomic bomb.", + dayOffset: 10, + hours: 0, + image: { + src: "https://image.tmdb.org/t/p/w300/8Gxv8gSFCU0XGDykEGv7zR1n2ua.jpg", + aspectRatio: { width: 7, height: 12 }, + }, + metadata: { type: "radarr", releaseType: "physicalRelease" }, + indicatorColor: "orange", + links: [ + { + href: "https://www.imdb.com/title/tt15398776/", + name: "IMDb", + color: "#f5c518", + isDark: false, + logo: "/images/apps/imdb.svg", + }, + ], + }, + { title: "Homarr Meetup", subTitle: "", description: "Yearly meetup of the Homarr community", - startDate, - endDate, - image: null, + dayOffset: 14, + hours: 14, location: "Mountains", + image: null, indicatorColor: "#fa5252", links: [ { @@ -29,63 +135,79 @@ const homarrMeetup = (start: Date, end: Date): CalendarEvent => { isDark: true, }, ], - }; + }, + { + title: "The Bear", + subTitle: "Season 3", + description: "A young chef fights to transform a sandwich shop.", + dayOffset: 18, + hours: 3, + image: { + src: "https://image.tmdb.org/t/p/w300/sHFlYJhNbmpMOkr7JKGxFAnMPFm.jpg", + aspectRatio: { width: 7, height: 12 }, + badge: { content: "S3/E1", color: "yellow" }, + }, + indicatorColor: "yellow", + links: [], + }, + { + title: "Foundation", + subTitle: "Season 3", + description: "A band of exiles discovers that the only way to save the Galactic Empire is to defy it.", + dayOffset: 21, + hours: 20, + image: { + src: "https://image.tmdb.org/t/p/w300/wSByZTwCPVYAjlqcVCBTCLZchkC.jpg", + aspectRatio: { width: 7, height: 12 }, + badge: { content: "S3/E1", color: "indigo" }, + }, + indicatorColor: "indigo", + links: [], + }, + { + title: "Blade Runner 2099", + subTitle: "Premiere", + description: "A new chapter in the Blade Runner universe.", + dayOffset: 25, + hours: 21, + image: { + src: "https://image.tmdb.org/t/p/w300/63UMsIFOlCSz3uCPKtbgOdUr4pE.jpg", + aspectRatio: { width: 7, height: 12 }, + }, + metadata: { type: "radarr", releaseType: "digitalRelease" }, + indicatorColor: "grape", + links: [], + }, +]; + +const buildEventsForRange = (start: Date, end: Date): CalendarEvent[] => { + const rangeStart = new Date(start); + rangeStart.setDate(rangeStart.getDate() - 3); + + return eventTemplates + .map((tpl) => { + const startDate = new Date(rangeStart); + startDate.setDate(startDate.getDate() + tpl.dayOffset); + startDate.setHours(tpl.hours, 0, 0, 0); + + return { + title: tpl.title, + subTitle: tpl.subTitle, + description: tpl.description, + startDate, + endDate: tpl.location ? new Date(startDate.getTime() + 2 * 60 * 60 * 1000) : null, + image: tpl.image, + location: tpl.location ?? null, + indicatorColor: tpl.indicatorColor, + links: tpl.links, + ...(tpl.metadata ? { metadata: tpl.metadata } : {}), + }; + }) + .filter((event) => event.startDate >= start && event.startDate <= end); }; -const titanicRelease = (start: Date, end: Date): CalendarEvent => ({ - title: "Titanic", - subTitle: "A classic movie", - description: "A tragic love story set on the ill-fated RMS Titanic.", - startDate: randomDateBetween(start, end), - endDate: null, - image: { - src: "https://image.tmdb.org/t/p/original/5bTWA20cL9LCIGNpde4Epc2Ijzn.jpg", - aspectRatio: { width: 7, height: 12 }, - }, - location: null, - metadata: { - type: "radarr", - releaseType: "inCinemas", - }, - indicatorColor: "cyan", - links: [ - { - href: "https://www.imdb.com/title/tt0120338/", - name: "IMDb", - color: "#f5c518", - isDark: false, - logo: "/images/apps/imdb.svg", - }, - ], -}); - -const seriesRelease = (start: Date, end: Date): CalendarEvent => ({ - title: "The Mandalorian", - subTitle: "A Star Wars Series", - description: "A lone bounty hunter in the outer reaches of the galaxy.", - startDate: randomDateBetween(start, end), - endDate: null, - image: { - src: "https://image.tmdb.org/t/p/original/sWgBv7LV2PRoQgkxwlibdGXKz1S.jpg", - aspectRatio: { width: 7, height: 12 }, - badge: { - content: "S1/E1", - color: "red", - }, - }, - location: null, - indicatorColor: "blue", - links: [ - { - href: "https://www.imdb.com/title/tt8111088/", - name: "IMDb", - color: "#f5c518", - isDark: false, - logo: "/images/apps/imdb.svg", - }, - ], -}); - -function randomDateBetween(start: Date, end: Date): Date { - return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())); +export class CalendarMockService implements ICalendarIntegration { + public async getCalendarEventsAsync(start: Date, end: Date, _includeUnmonitored: boolean): Promise { + return await Promise.resolve(buildEventsForRange(start, end)); + } } diff --git a/packages/integrations/src/mock/data/download.ts b/packages/integrations/src/mock/data/download.ts index 7d3ab0252..391315fe4 100644 --- a/packages/integrations/src/mock/data/download.ts +++ b/packages/integrations/src/mock/data/download.ts @@ -2,57 +2,163 @@ import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/dow import type { IDownloadClientIntegration } from "../../interfaces/downloads/download-client-integration"; import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items"; +const mockItems: Omit[] = [ + { + name: "Big.Buck.Bunny.2008.1080p.BluRay.x264", + type: "torrent", + size: 4_700_000_000, + progress: 0.47, + state: "leeching", + downSpeed: 12_400_000, + upSpeed: 890_000, + sent: 420_000_000, + received: 2_209_000_000, + time: 201_000, + category: "movies", + }, + { + name: "Ubuntu.24.04.1.LTS.Desktop.amd64.iso", + type: "torrent", + size: 5_800_000_000, + progress: 0.82, + state: "leeching", + downSpeed: 34_500_000, + upSpeed: 2_100_000, + sent: 1_200_000_000, + received: 4_756_000_000, + time: 30_000, + category: "iso", + }, + { + name: "Sintel.2010.4K.DTS-HD.MA.5.1.mkv", + type: "torrent", + size: 18_200_000_000, + progress: 0.03, + state: "leeching", + downSpeed: 1_200_000, + upSpeed: 50_000, + sent: 30_000_000, + received: 546_000_000, + time: 14_880_000, + category: "movies", + }, + { + name: "Arch.Linux.2024.12.01.x86_64.iso", + type: "torrent", + size: 1_100_000_000, + progress: 1, + state: "seeding", + downSpeed: 0, + upSpeed: 4_500_000, + sent: 3_300_000_000, + received: 1_100_000_000, + time: -7_200_000, + category: "iso", + }, + { + name: "Tears.of.Steel.2012.1080p.DTS.mkv", + type: "torrent", + size: 7_340_000_000, + progress: 0, + state: "paused", + downSpeed: 0, + upSpeed: 0, + time: 0, + category: "movies", + }, + { + name: "Cosmos.Laundromat.First.Cycle.2015.4K.mkv", + type: "torrent", + size: 12_000_000_000, + progress: 0.65, + state: "stalled", + downSpeed: 0, + upSpeed: 0, + sent: 200_000_000, + received: 7_800_000_000, + time: 0, + category: "movies", + }, + { + name: "Spring.2019.4K.HDR.mkv", + type: "usenet", + size: 3_200_000_000, + progress: 0.91, + state: "downloading", + downSpeed: 45_000_000, + time: 6_500, + category: "movies", + }, + { + name: "Agent.327.Operation.Barbershop.2017.1080p.mkv", + type: "usenet", + size: 1_800_000_000, + progress: 1, + state: "completed", + time: -3_600_000, + category: "movies", + }, + { + name: "Caminandes.3.Llamigos.2016.1080p.mkv", + type: "torrent", + size: 890_000_000, + progress: 1, + state: "seeding", + downSpeed: 0, + upSpeed: 1_800_000, + sent: 2_670_000_000, + received: 890_000_000, + time: -86_400_000, + category: "movies", + }, + { + name: "Hero.2024.S01.1080p.WEB-DL.DDP5.1", + type: "usenet", + size: 9_400_000_000, + progress: 0.22, + state: "downloading", + downSpeed: 28_000_000, + time: 244_000, + category: "tv", + }, +]; + export class DownloadClientMockService implements IDownloadClientIntegration { public async getClientJobsAndStatusAsync(input: { limit: number }): Promise { return await Promise.resolve({ status: { - paused: Math.random() < 0.5, + paused: false, rates: { - down: Math.floor(Math.random() * 5000), - up: Math.floor(Math.random() * 5000), + down: 121_100_000, + up: 9_340_000, }, - types: ["torrent", "usenet"], + types: ["torrent", "usenet"] as const, }, - items: Array.from({ length: 20 }, (_, index) => DownloadClientMockService.createItem(index)).slice( - 0, - input.limit, - ), + items: mockItems.slice(0, input.limit).map((item, index) => ({ + ...item, + id: `mock-${index}`, + index, + })), }); } public async pauseQueueAsync(): Promise { - return await Promise.resolve(); + await Promise.resolve(); } public async pauseItemAsync(_item: DownloadClientItem): Promise { - return await Promise.resolve(); + await Promise.resolve(); } public async resumeQueueAsync(): Promise { - return Promise.resolve(); + await Promise.resolve(); } public async resumeItemAsync(_item: DownloadClientItem): Promise { - return await Promise.resolve(); + await Promise.resolve(); } public async deleteItemAsync(_item: DownloadClientItem, _fromDisk: boolean): Promise { - return await Promise.resolve(); - } - - private static createItem(index: number): DownloadClientItem { - const progress = Math.random() < 0.5 ? Math.random() : 1; - return { - id: `item-${index}`, - index, - name: `Item ${index}`, - type: Math.random() > 0.5 ? "torrent" : "usenet", - progress, - size: Math.floor(Math.random() * 10000) + 1, - downSpeed: Math.floor(Math.random() * 5000), - upSpeed: Math.floor(Math.random() * 5000), - state: progress >= 1 ? "completed" : "downloading", - time: 0, - }; + await Promise.resolve(); } } diff --git a/packages/integrations/src/mock/data/media-releases.ts b/packages/integrations/src/mock/data/media-releases.ts index d820a9aa4..15504b225 100644 --- a/packages/integrations/src/mock/data/media-releases.ts +++ b/packages/integrations/src/mock/data/media-releases.ts @@ -13,17 +13,17 @@ export const mockMediaReleases: MediaRelease[] = [ title: "Inception", subtitle: "A mind-bending thriller", description: - "A thief who steals corporate secrets through the use of dream-sharing technology is given the inverse task of planting an idea into the mind of a CEO.", + "A thief who steals corporate secrets through dream-sharing technology is given the task of planting an idea into the mind of a CEO.", releaseDate: new Date("2010-07-16"), imageUrls: { - poster: "https://media.outnow.ch/Movies/Bilder/2025/MinecraftMovie/015.jpg", - backdrop: "https://example.com/inception_backdrop.jpg", + poster: "https://image.tmdb.org/t/p/w300/ljsZTbVsrQSqZgWeep2B1QiDKuh.jpg", + backdrop: "https://image.tmdb.org/t/p/w780/8ZTVqvKDQ8emSGUEMjsS4yHAwrp.jpg", }, producer: "Warner Bros.", price: 14.99, rating: "8.8/10", tags: ["Sci-Fi", "Thriller"], - href: "https://example.com/inception", + href: "https://www.themoviedb.org/movie/27205-inception", length: 148, }, { @@ -34,13 +34,13 @@ export const mockMediaReleases: MediaRelease[] = [ description: "When Walter White's secret is revealed, he must face the consequences of his actions.", releaseDate: new Date("2013-09-15"), imageUrls: { - poster: "https://media.outnow.ch/Movies/Bilder/2025/MinecraftMovie/015.jpg", - backdrop: "https://example.com/breaking_bad_backdrop.jpg", + poster: "https://image.tmdb.org/t/p/w300/ztkUQFLlC19CCMYHW73GM9hBNRh.jpg", + backdrop: "https://image.tmdb.org/t/p/w780/tsRy63Mu5cu8etL1X7ZLyf7UP1M.jpg", }, producer: "AMC", rating: "9.5/10", tags: ["Crime", "Drama"], - href: "https://example.com/breaking_bad", + href: "https://www.themoviedb.org/tv/1396-breaking-bad", }, { id: "3", @@ -50,79 +50,86 @@ export const mockMediaReleases: MediaRelease[] = [ description: "The fourth studio album by French electronic music duo Daft Punk.", releaseDate: new Date("2013-05-17"), imageUrls: { - poster: "https://media.outnow.ch/Movies/Bilder/2025/MinecraftMovie/015.jpg", - backdrop: "https://example.com/ram_backdrop.jpg", + poster: "https://image.tmdb.org/t/p/w300/mIfBmEBqA1ddSMFyMAvACvOQ6cL.jpg", + backdrop: "https://image.tmdb.org/t/p/w780/mIfBmEBqA1ddSMFyMAvACvOQ6cL.jpg", }, producer: "Columbia Records", price: 9.99, rating: "8.5/10", - tags: ["Electronic", "Dance", "Pop", "Funk"], - href: "https://example.com/ram", + tags: ["Electronic", "Funk"], + href: "https://www.daftpunk.com", }, { id: "4", - type: "book", - title: "The Great Gatsby", - subtitle: "F. Scott Fitzgerald", - description: "A novel about the American dream and the disillusionment that comes with it.", - releaseDate: new Date("1925-04-10"), + type: "movie", + title: "The Dark Knight", + subtitle: "Theatrical Release", + description: + "Batman raises the stakes in his war on crime. With the help of Lt. Gordon and DA Harvey Dent, Batman sets out to dismantle organized crime.", + releaseDate: new Date("2008-07-18"), imageUrls: { - poster: "https://media.outnow.ch/Movies/Bilder/2025/MinecraftMovie/015.jpg", - backdrop: "https://example.com/gatsby_backdrop.jpg", + poster: "https://image.tmdb.org/t/p/w300/qJ2tW6WMUDux911BTUgMe1nV8Is.jpg", + backdrop: "https://image.tmdb.org/t/p/w780/nMKdUUepR0i5zn0y1T4CsSB5ez.jpg", }, - producer: "Scribner", - price: 10.99, - rating: "4.2/5", - tags: ["Classic", "Fiction"], - href: "https://example.com/gatsby", + producer: "Warner Bros.", + price: 12.99, + rating: "9.0/10", + tags: ["Action", "Crime", "Drama"], + href: "https://www.themoviedb.org/movie/155-the-dark-knight", + length: 152, }, { id: "5", - type: "game", - title: "The Legend of Zelda: Breath of the Wild", - subtitle: "Nintendo Switch", - description: "An open-world action-adventure game set in the fantasy land of Hyrule.", - releaseDate: new Date("2017-03-03"), + type: "movie", + title: "Interstellar", + subtitle: "Digital Release", + description: "A team of explorers travel through a wormhole in space in an attempt to ensure humanity's survival.", + releaseDate: new Date("2014-11-07"), imageUrls: { - poster: "https://media.outnow.ch/Movies/Bilder/2025/MinecraftMovie/015.jpg", - backdrop: "https://example.com/zelda_backdrop.jpg", + poster: "https://image.tmdb.org/t/p/w300/gEU2QniE6E77NI6lCU6MxlNBvIx.jpg", + backdrop: "https://image.tmdb.org/t/p/w780/xJHokMbljXjADYdit5fK1DVfjko.jpg", }, - producer: "Nintendo", - price: 59.99, - rating: "10/10", - tags: ["Action", "Adventure"], - href: "https://example.com/zelda", + producer: "Paramount", + price: 14.99, + rating: "8.7/10", + tags: ["Sci-Fi", "Adventure"], + href: "https://www.themoviedb.org/movie/157336-interstellar", + length: 169, }, { id: "6", - type: "article", - title: "The Rise of AI in Healthcare", - subtitle: "Tech Innovations", - description: "Exploring the impact of artificial intelligence on the healthcare industry.", - releaseDate: new Date("2023-10-01"), + type: "tv", + title: "Severance", + subtitle: "S2E1 - Premiere", + description: + "Mark leads a team of office workers whose memories are surgically divided between work and personal lives.", + releaseDate: new Date("2025-01-17"), imageUrls: { - poster: "https://media.outnow.ch/Movies/Bilder/2025/MinecraftMovie/015.jpg", - backdrop: "https://example.com/ai_healthcare_backdrop.jpg", + poster: "https://image.tmdb.org/t/p/w300/pJAtR4gMFnalHCmT9cByPFgkZ6B.jpg", + backdrop: "https://image.tmdb.org/t/p/w780/gvaAaDUbF41Z1NLuJsHPRHkv1Ie.jpg", }, - producer: "Tech Innovations", - rating: "4.8/5", - tags: ["Technology", "Healthcare"], - href: "https://example.com/ai_healthcare", + producer: "Apple TV+", + rating: "8.7/10", + tags: ["Thriller", "Sci-Fi"], + href: "https://www.themoviedb.org/tv/95396-severance", }, { id: "7", - type: "video", - title: "Wir LIEBEN unsere MAMAS | 50 Fragen zu Mamas", - releaseDate: new Date("2024-05-18T17:00:00Z"), + type: "movie", + title: "Dune: Part Two", + subtitle: "Theatrical Release", + description: + "Paul Atreides unites with the Fremen to seek revenge against the conspirators who destroyed his family.", + releaseDate: new Date("2024-03-01"), imageUrls: { - poster: - "https://i.ytimg.com/vi/a3qyfXc1Pfg/hq720.jpg?sqp=-oaymwEnCNAFEJQDSFryq4qpAxkIARUAAIhCGAHYAQHiAQoIGBACGAY4AUAB&rs=AOn4CLBQKm0viRlfRjTV-V24vGO83rPaVw", - backdrop: - "https://i.ytimg.com/vi/a3qyfXc1Pfg/hq720.jpg?sqp=-oaymwEnCNAFEJQDSFryq4qpAxkIARUAAIhCGAHYAQHiAQoIGBACGAY4AUAB&rs=AOn4CLBQKm0viRlfRjTV-V24vGO83rPaVw", + poster: "https://image.tmdb.org/t/p/w300/8b8R8l88Qje9dn9OE8PY05Nez7.jpg", + backdrop: "https://image.tmdb.org/t/p/w780/xOMo8BRK7PfcJv9JCnx7s5hj0PX.jpg", }, - producer: "PietSmiet", - rating: "1K", - tags: [], - href: "https://www.youtube.com/watch?v=a3qyfXc1Pfg", + producer: "Warner Bros.", + price: 19.99, + rating: "8.5/10", + tags: ["Sci-Fi", "Adventure"], + href: "https://www.themoviedb.org/movie/693134-dune-part-two", + length: 166, }, ]; diff --git a/packages/integrations/src/mock/data/media-server.ts b/packages/integrations/src/mock/data/media-server.ts index 511d47f59..96640d1ae 100644 --- a/packages/integrations/src/mock/data/media-server.ts +++ b/packages/integrations/src/mock/data/media-server.ts @@ -1,36 +1,95 @@ import type { IMediaServerIntegration } from "../../interfaces/media-server/media-server-integration"; import type { CurrentSessionsInput, StreamSession } from "../../interfaces/media-server/media-server-types"; -export class MediaServerMockService implements IMediaServerIntegration { - public async getCurrentSessionsAsync(options: CurrentSessionsInput): Promise { - return await Promise.resolve( - Array.from({ length: 10 }, (_, index) => MediaServerMockService.createSession(index)).filter( - (session) => !options.showOnlyPlaying || session.currentlyPlaying !== null, - ), - ); - } +const sessions: StreamSession[] = [ + { + sessionId: "s-0", + sessionName: "Living Room TV", + user: { userId: "u-0", username: "thomas", profilePictureUrl: null }, + currentlyPlaying: { + type: "movie", + name: "Interstellar", + seasonName: undefined, + episodeName: null, + albumName: null, + episodeCount: null, + metadata: null, + }, + }, + { + sessionId: "s-1", + sessionName: "Bedroom Chromecast", + user: { userId: "u-1", username: "sarah", profilePictureUrl: null }, + currentlyPlaying: { + type: "video", + name: "Breaking Bad", + seasonName: "Season 5", + episodeName: "Ozymandias", + albumName: null, + episodeCount: null, + metadata: null, + }, + }, + { + sessionId: "s-2", + sessionName: "Office Desktop", + user: { userId: "u-2", username: "mike", profilePictureUrl: null }, + currentlyPlaying: { + type: "movie", + name: "The Dark Knight", + seasonName: undefined, + episodeName: null, + albumName: null, + episodeCount: null, + metadata: null, + }, + }, + { + sessionId: "s-3", + sessionName: "iPad", + user: { userId: "u-3", username: "emily", profilePictureUrl: null }, + currentlyPlaying: { + type: "video", + name: "Severance", + seasonName: "Season 2", + episodeName: "The We We Are", + albumName: null, + episodeCount: null, + metadata: null, + }, + }, + { + sessionId: "s-4", + sessionName: "Mobile", + user: { userId: "u-4", username: "alex", profilePictureUrl: null }, + currentlyPlaying: { + type: "audio", + name: "Daft Punk", + seasonName: undefined, + episodeName: null, + albumName: "Random Access Memories", + episodeCount: null, + metadata: null, + }, + }, + { + sessionId: "s-5", + sessionName: "Guest Room", + user: { userId: "u-5", username: "guest", profilePictureUrl: null }, + currentlyPlaying: { + type: "movie", + name: "Oppenheimer", + seasonName: undefined, + episodeName: null, + albumName: null, + episodeCount: null, + metadata: null, + }, + }, +]; - private static createSession(index: number): StreamSession { - return { - sessionId: `session-${index}`, - sessionName: `Session ${index}`, - user: { - userId: `user-${index}`, - username: `User${index}`, - profilePictureUrl: "/images/mock/avatar.jpg", - }, - currentlyPlaying: - Math.random() > 0.9 // 10% chance of being null (not currently playing) - ? { - type: "movie", - name: `Movie ${index}`, - seasonName: undefined, - episodeName: null, - albumName: null, - episodeCount: null, - metadata: null, - } - : null, - }; +export class MediaServerMockService implements IMediaServerIntegration { + public async getCurrentSessionsAsync(_options: CurrentSessionsInput): Promise { + return await Promise.resolve(sessions); } } diff --git a/packages/integrations/src/mock/data/network-controller-summary.ts b/packages/integrations/src/mock/data/network-controller-summary.ts index 7ffe80c71..59d54999a 100644 --- a/packages/integrations/src/mock/data/network-controller-summary.ts +++ b/packages/integrations/src/mock/data/network-controller-summary.ts @@ -4,27 +4,11 @@ import type { NetworkControllerSummary } from "../../types"; export class NetworkControllerSummaryMockService implements NetworkControllerSummaryIntegration { public async getNetworkSummaryAsync(): Promise { return await Promise.resolve({ - lan: { - guests: 5, - users: 10, - status: "enabled", - }, - vpn: { - users: 3, - status: "enabled", - }, - wanStatus: "disabled", - wifi: { - status: "disabled", - guests: 0, - users: 0, - }, - www: { - latency: 22, - status: "enabled", - ping: 32, - uptime: 3600, - }, + lan: { guests: 5, users: 18, status: "enabled" }, + vpn: { users: 3, status: "enabled" }, + wanStatus: "enabled", + wifi: { status: "enabled", guests: 2, users: 12 }, + www: { latency: 22, status: "enabled", ping: 14, uptime: 864000 }, }); } } diff --git a/packages/integrations/src/mock/mock-integration.ts b/packages/integrations/src/mock/mock-integration.ts index f04959171..fd1cbe0ea 100644 --- a/packages/integrations/src/mock/mock-integration.ts +++ b/packages/integrations/src/mock/mock-integration.ts @@ -85,7 +85,7 @@ export class MockIntegration // Health Monitoring Integrations getSystemInfoAsync = MockIntegration.systemMonitoring.getSystemInfoAsync.bind(MockIntegration.systemMonitoring); - getClusterInfoAsync = MockIntegration.clusterMonitoring.getClusterInfoAsync.bind(MockIntegration.downloadClient); + getClusterInfoAsync = MockIntegration.clusterMonitoring.getClusterInfoAsync.bind(MockIntegration.clusterMonitoring); // IndexerManagerIntegration getIndexersAsync = MockIntegration.indexerManager.getIndexersAsync.bind(MockIntegration.indexerManager); @@ -104,7 +104,7 @@ export class MockIntegration declineRequestAsync = MockIntegration.mediaRequest.declineRequestAsync.bind(MockIntegration.mediaRequest); // MediaServerIntegration - getCurrentSessionsAsync = MockIntegration.mediaServer.getCurrentSessionsAsync.bind(MockIntegration.mediaRequest); + getCurrentSessionsAsync = MockIntegration.mediaServer.getCurrentSessionsAsync.bind(MockIntegration.mediaServer); // MediaTranscodingIntegration getStatisticsAsync = MockIntegration.mediaTranscoding.getStatisticsAsync.bind(MockIntegration.mediaTranscoding); diff --git a/packages/notifications/src/index.tsx b/packages/notifications/src/index.tsx index 071496b20..41bb16964 100644 --- a/packages/notifications/src/index.tsx +++ b/packages/notifications/src/index.tsx @@ -1,6 +1,6 @@ import type { NotificationData } from "@mantine/notifications"; import { notifications } from "@mantine/notifications"; -import { IconCheck, IconX } from "@tabler/icons-react"; +import { IconCheck, IconInfoCircle, IconX } from "@tabler/icons-react"; export const showSuccessNotification = (props: NotificationData) => notifications.show({ @@ -15,3 +15,10 @@ export const showErrorNotification = (props: NotificationData) => color: "red", icon: , }); + +export const showWarningNotification = (props: NotificationData) => + notifications.show({ + ...props, + color: "yellow", + icon: , + }); diff --git a/packages/request-handler/src/docker.ts b/packages/request-handler/src/docker.ts index a31eed56c..d2b24466c 100644 --- a/packages/request-handler/src/docker.ts +++ b/packages/request-handler/src/docker.ts @@ -7,17 +7,174 @@ import { createLogger } from "@homarr/core/infrastructure/logs"; import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; import { db, like, or } from "@homarr/db"; import { icons } from "@homarr/db/schema"; -import type { ContainerState } from "@homarr/docker"; +import type { ContainerState, Port } from "@homarr/docker"; import { dockerLabels, DockerSingleton } from "@homarr/docker"; import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; const logger = createLogger({ module: "dockerRequestHandler" }); +const isDemoMode = ["1", "yes", "t", "true"].includes((process.env.DEMO_MODE ?? "").toLowerCase()); + +const port = (privatePort: number, publicPort: number, type: string): Port => ({ + IP: "0.0.0.0", + PrivatePort: privatePort, + PublicPort: publicPort, + Type: type, +}); + +const mockContainers: { + id: string; + name: string; + host: string; + state: ContainerState; + image: string; + iconUrl: string; + cpuUsage: number; + memoryUsage: number; + ports: Port[]; +}[] = [ + { + id: "a1b2c3d4e5f6", + name: "sonarr", + host: "local", + state: "running", + image: "lscr.io/linuxserver/sonarr:latest", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/sonarr.svg", + cpuUsage: 2.3, + memoryUsage: 256 * 1024 * 1024, + ports: [port(8989, 8989, "tcp")], + }, + { + id: "b2c3d4e5f6a7", + name: "radarr", + host: "local", + state: "running", + image: "lscr.io/linuxserver/radarr:latest", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/radarr.svg", + cpuUsage: 1.8, + memoryUsage: 220 * 1024 * 1024, + ports: [port(7878, 7878, "tcp")], + }, + { + id: "c3d4e5f6a7b8", + name: "plex", + host: "local", + state: "running", + image: "lscr.io/linuxserver/plex:latest", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/plex.svg", + cpuUsage: 12.5, + memoryUsage: 1024 * 1024 * 1024, + ports: [port(32400, 32400, "tcp")], + }, + { + id: "d4e5f6a7b8c9", + name: "qbittorrent", + host: "local", + state: "running", + image: "lscr.io/linuxserver/qbittorrent:latest", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/qbittorrent.svg", + cpuUsage: 5.1, + memoryUsage: 380 * 1024 * 1024, + ports: [port(8080, 8080, "tcp")], + }, + { + id: "e5f6a7b8c9d0", + name: "prowlarr", + host: "local", + state: "running", + image: "lscr.io/linuxserver/prowlarr:latest", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/prowlarr.svg", + cpuUsage: 0.8, + memoryUsage: 120 * 1024 * 1024, + ports: [port(9696, 9696, "tcp")], + }, + { + id: "f6a7b8c9d0e1", + name: "overseerr", + host: "local", + state: "running", + image: "lscr.io/linuxserver/overseerr:latest", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/overseerr.svg", + cpuUsage: 1.2, + memoryUsage: 180 * 1024 * 1024, + ports: [port(5055, 5055, "tcp")], + }, + { + id: "a7b8c9d0e1f2", + name: "homarr", + host: "local", + state: "running", + image: "ghcr.io/homarr-labs/homarr:latest", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/homarr.svg", + cpuUsage: 3.4, + memoryUsage: 290 * 1024 * 1024, + ports: [port(7575, 7575, "tcp")], + }, + { + id: "b8c9d0e1f2a3", + name: "pihole", + host: "local", + state: "running", + image: "pihole/pihole:latest", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/pi-hole.svg", + cpuUsage: 0.5, + memoryUsage: 95 * 1024 * 1024, + ports: [port(80, 80, "tcp"), port(53, 53, "udp")], + }, + { + id: "c9d0e1f2a3b4", + name: "nginx-proxy", + host: "local", + state: "running", + image: "jc21/nginx-proxy-manager:latest", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/nginx-proxy-manager.svg", + cpuUsage: 0.3, + memoryUsage: 65 * 1024 * 1024, + ports: [port(443, 443, "tcp"), port(81, 81, "tcp")], + }, + { + id: "d0e1f2a3b4c5", + name: "watchtower", + host: "local", + state: "running", + image: "containrrr/watchtower:latest", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/watchtower.svg", + cpuUsage: 0.1, + memoryUsage: 30 * 1024 * 1024, + ports: [], + }, + { + id: "e1f2a3b4c5d6", + name: "tdarr", + host: "local", + state: "exited", + image: "ghcr.io/haveagitgat/tdarr:latest", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/tdarr.svg", + cpuUsage: 0, + memoryUsage: 0, + ports: [], + }, + { + id: "f2a3b4c5d6e7", + name: "bazarr", + host: "local", + state: "running", + image: "lscr.io/linuxserver/bazarr:latest", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/bazarr.svg", + cpuUsage: 0.6, + memoryUsage: 110 * 1024 * 1024, + ports: [port(6767, 6767, "tcp")], + }, +]; + export const dockerContainersRequestHandler = createCachedWidgetRequestHandler({ queryKey: "dockerContainersResult", widgetKind: "dockerContainers", async requestAsync() { + if (isDemoMode) { + return mockContainers; + } return await getContainersWithStatsAsync(); }, cacheDuration: dayjs.duration(20, "seconds"),