From 0ad7b79187913c0739e32977ec33dcab761fb8f9 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Fri, 5 Jun 2026 15:38:27 +0200 Subject: [PATCH] fix: address PR review feedback on mock services - Restore `await Promise.resolve()` in all async mock methods to satisfy require-await lint rule (download, calendar, media-releases, media-server, network-controller-summary) - Fix Docker mock container ports to include required `IP` field and use proper `Port` type from dockerode - Add app widgets for bookmarked apps to the demo dashboard - Reposition bookmarks widget (6 wide, after Proxmox) and allow edit mode with discard-on-exit in demo mode --- .../app/[locale]/_client-providers/trpc.tsx | 6 +- apps/nextjs/src/app/[locale]/layout.tsx | 1 - packages/db/migrations/seed.ts | 100 ++++++++--- .../integrations/src/mock/data/calendar.ts | 88 +++++++--- .../integrations/src/mock/data/download.ts | 26 ++- .../src/mock/data/media-releases.ts | 14 +- .../src/mock/data/media-server.ts | 92 +++++++++- .../mock/data/network-controller-summary.ts | 4 +- packages/request-handler/src/docker.ts | 165 ++++++++++++++++-- 9 files changed, 413 insertions(+), 83 deletions(-) diff --git a/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx b/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx index 5aa145093..7c7ca0766 100644 --- a/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx +++ b/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx @@ -61,7 +61,11 @@ export function TRPCReactProvider(props: PropsWithChildren) { }, mutations: { onError(error) { - if (error instanceof TRPCClientError && error.data?.code === "FORBIDDEN" && error.message === "Mutations are disabled in demo mode") { + 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]/layout.tsx b/apps/nextjs/src/app/[locale]/layout.tsx index b8ea0a33a..ac7d8d856 100644 --- a/apps/nextjs/src/app/[locale]/layout.tsx +++ b/apps/nextjs/src/app/[locale]/layout.tsx @@ -23,7 +23,6 @@ import { SpotlightProvider } from "@homarr/spotlight"; import type { SupportedLanguage } from "@homarr/translation"; import { isLocaleRTL, isLocaleSupported } from "@homarr/translation"; - import { Analytics } from "~/components/layout/analytics"; import { CrowdinLiveTranslation } from "~/components/layout/crowdin-live-translation"; diff --git a/packages/db/migrations/seed.ts b/packages/db/migrations/seed.ts index 7aa615fdc..0a00e9674 100644 --- a/packages/db/migrations/seed.ts +++ b/packages/db/migrations/seed.ts @@ -226,18 +226,66 @@ interface DemoWidget { } 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" }, + { + 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[] => [ @@ -248,7 +296,10 @@ const buildDemoWidgets = (appIds: string[]): DemoWidget[] => [ // Row 2: healthMonitoring + bookmarks = 12 { kind: "healthMonitoring", width: 6, height: 3, needsIntegration: true }, { - kind: "bookmarks", width: 6, height: 3, needsIntegration: false, + kind: "bookmarks", + width: 6, + height: 3, + needsIntegration: false, options: { title: "Homelab", items: appIds, layout: "grid", openNewTab: true }, }, // Row 3: mediaServer + mediaTranscoding + dnsHoleSummary = 12 @@ -267,22 +318,27 @@ const buildDemoWidgets = (appIds: string[]): DemoWidget[] => [ { kind: "networkControllerSummary", width: 3, height: 2, needsIntegration: true }, { kind: "networkControllerStatus", width: 3, height: 2, needsIntegration: true }, { - kind: "rssFeed", width: 6, height: 2, needsIntegration: false, + kind: "rssFeed", + width: 6, + height: 2, + needsIntegration: false, options: { - feedUrls: [ - "https://selfh.st/rss/", - "https://hnrss.org/newest?q=self-hosted", - ], + 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 }, - })), + ...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 }, diff --git a/packages/integrations/src/mock/data/calendar.ts b/packages/integrations/src/mock/data/calendar.ts index 038827c79..4ebd36d6e 100644 --- a/packages/integrations/src/mock/data/calendar.ts +++ b/packages/integrations/src/mock/data/calendar.ts @@ -27,7 +27,15 @@ const eventTemplates: EventTemplate[] = [ 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" }], + links: [ + { + href: "https://www.imdb.com/title/tt8111088/", + name: "IMDb", + color: "#f5c518", + isDark: false, + logo: "/images/apps/imdb.svg", + }, + ], }, { title: "Silo", @@ -41,7 +49,15 @@ const eventTemplates: EventTemplate[] = [ 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" }], + links: [ + { + href: "https://www.imdb.com/title/tt14688458/", + name: "IMDb", + color: "#f5c518", + isDark: false, + logo: "/images/apps/imdb.svg", + }, + ], }, { title: "Dune: Part Two", @@ -55,7 +71,15 @@ const eventTemplates: EventTemplate[] = [ }, 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" }], + links: [ + { + href: "https://www.imdb.com/title/tt15239678/", + name: "IMDb", + color: "#f5c518", + isDark: false, + logo: "/images/apps/imdb.svg", + }, + ], }, { title: "Severance", @@ -83,7 +107,15 @@ const eventTemplates: EventTemplate[] = [ }, 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" }], + links: [ + { + href: "https://www.imdb.com/title/tt15398776/", + name: "IMDb", + color: "#f5c518", + isDark: false, + logo: "/images/apps/imdb.svg", + }, + ], }, { title: "Homarr Meetup", @@ -94,7 +126,15 @@ const eventTemplates: EventTemplate[] = [ location: "Mountains", image: null, indicatorColor: "#fa5252", - links: [{ href: "https://homarr.dev", name: "Homarr", logo: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/homarr.svg", color: "#000000", isDark: true }], + links: [ + { + href: "https://homarr.dev", + name: "Homarr", + logo: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/homarr.svg", + color: "#000000", + isDark: true, + }, + ], }, { title: "The Bear", @@ -144,28 +184,30 @@ 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 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); + 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); }; export class CalendarMockService implements ICalendarIntegration { public async getCalendarEventsAsync(start: Date, end: Date, _includeUnmonitored: boolean): Promise { - return buildEventsForRange(start, end); + 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 2f87b7972..391315fe4 100644 --- a/packages/integrations/src/mock/data/download.ts +++ b/packages/integrations/src/mock/data/download.ts @@ -125,30 +125,40 @@ const mockItems: Omit[] = [ export class DownloadClientMockService implements IDownloadClientIntegration { public async getClientJobsAndStatusAsync(input: { limit: number }): Promise { - return { + return await Promise.resolve({ status: { paused: false, rates: { down: 121_100_000, up: 9_340_000, }, - types: ["torrent", "usenet"], + types: ["torrent", "usenet"] as const, }, items: mockItems.slice(0, input.limit).map((item, index) => ({ ...item, id: `mock-${index}`, index, })), - }; + }); } - public async pauseQueueAsync(): Promise {} + public async pauseQueueAsync(): Promise { + await Promise.resolve(); + } - public async pauseItemAsync(_item: DownloadClientItem): Promise {} + public async pauseItemAsync(_item: DownloadClientItem): Promise { + await Promise.resolve(); + } - public async resumeQueueAsync(): Promise {} + public async resumeQueueAsync(): Promise { + await Promise.resolve(); + } - public async resumeItemAsync(_item: DownloadClientItem): Promise {} + public async resumeItemAsync(_item: DownloadClientItem): Promise { + await Promise.resolve(); + } - public async deleteItemAsync(_item: DownloadClientItem, _fromDisk: boolean): Promise {} + public async deleteItemAsync(_item: DownloadClientItem, _fromDisk: boolean): Promise { + await Promise.resolve(); + } } diff --git a/packages/integrations/src/mock/data/media-releases.ts b/packages/integrations/src/mock/data/media-releases.ts index 8f5408207..15504b225 100644 --- a/packages/integrations/src/mock/data/media-releases.ts +++ b/packages/integrations/src/mock/data/media-releases.ts @@ -2,7 +2,7 @@ import type { IMediaReleasesIntegration, MediaRelease } from "../../interfaces/m export class MediaReleasesMockService implements IMediaReleasesIntegration { public async getMediaReleasesAsync(): Promise { - return mockMediaReleases; + return await Promise.resolve(mockMediaReleases); } } @@ -12,7 +12,8 @@ export const mockMediaReleases: MediaRelease[] = [ type: "movie", title: "Inception", subtitle: "A mind-bending thriller", - description: "A thief who steals corporate secrets through dream-sharing technology is given the task of planting an idea into the mind of a CEO.", + description: + "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://image.tmdb.org/t/p/w300/ljsZTbVsrQSqZgWeep2B1QiDKuh.jpg", @@ -63,7 +64,8 @@ export const mockMediaReleases: MediaRelease[] = [ 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.", + 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://image.tmdb.org/t/p/w300/qJ2tW6WMUDux911BTUgMe1nV8Is.jpg", @@ -99,7 +101,8 @@ export const mockMediaReleases: MediaRelease[] = [ 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.", + 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://image.tmdb.org/t/p/w300/pJAtR4gMFnalHCmT9cByPFgkZ6B.jpg", @@ -115,7 +118,8 @@ export const mockMediaReleases: MediaRelease[] = [ 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.", + 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://image.tmdb.org/t/p/w300/8b8R8l88Qje9dn9OE8PY05Nez7.jpg", diff --git a/packages/integrations/src/mock/data/media-server.ts b/packages/integrations/src/mock/data/media-server.ts index 18335dad7..96640d1ae 100644 --- a/packages/integrations/src/mock/data/media-server.ts +++ b/packages/integrations/src/mock/data/media-server.ts @@ -2,16 +2,94 @@ import type { IMediaServerIntegration } from "../../interfaces/media-server/medi import type { CurrentSessionsInput, StreamSession } from "../../interfaces/media-server/media-server-types"; 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 } }, + { + 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, + }, + }, ]; export class MediaServerMockService implements IMediaServerIntegration { public async getCurrentSessionsAsync(_options: CurrentSessionsInput): Promise { - return sessions; + 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 bb708d5e3..59d54999a 100644 --- a/packages/integrations/src/mock/data/network-controller-summary.ts +++ b/packages/integrations/src/mock/data/network-controller-summary.ts @@ -3,12 +3,12 @@ import type { NetworkControllerSummary } from "../../types"; export class NetworkControllerSummaryMockService implements NetworkControllerSummaryIntegration { public async getNetworkSummaryAsync(): Promise { - return { + return await Promise.resolve({ 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/request-handler/src/docker.ts b/packages/request-handler/src/docker.ts index 0f2ae1102..d2b24466c 100644 --- a/packages/request-handler/src/docker.ts +++ b/packages/request-handler/src/docker.ts @@ -7,7 +7,7 @@ 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"; @@ -16,19 +16,156 @@ const logger = createLogger({ module: "dockerRequestHandler" }); const isDemoMode = ["1", "yes", "t", "true"].includes((process.env.DEMO_MODE ?? "").toLowerCase()); -const mockContainers = [ - { id: "a1b2c3d4e5f6", name: "sonarr", host: "local", state: "running" as ContainerState, 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: [{ PrivatePort: 8989, PublicPort: 8989, Type: "tcp" }] }, - { id: "b2c3d4e5f6a7", name: "radarr", host: "local", state: "running" as ContainerState, 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: [{ PrivatePort: 7878, PublicPort: 7878, Type: "tcp" }] }, - { id: "c3d4e5f6a7b8", name: "plex", host: "local", state: "running" as ContainerState, 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: [{ PrivatePort: 32400, PublicPort: 32400, Type: "tcp" }] }, - { id: "d4e5f6a7b8c9", name: "qbittorrent", host: "local", state: "running" as ContainerState, 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: [{ PrivatePort: 8080, PublicPort: 8080, Type: "tcp" }] }, - { id: "e5f6a7b8c9d0", name: "prowlarr", host: "local", state: "running" as ContainerState, 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: [{ PrivatePort: 9696, PublicPort: 9696, Type: "tcp" }] }, - { id: "f6a7b8c9d0e1", name: "overseerr", host: "local", state: "running" as ContainerState, 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: [{ PrivatePort: 5055, PublicPort: 5055, Type: "tcp" }] }, - { id: "a7b8c9d0e1f2", name: "homarr", host: "local", state: "running" as ContainerState, 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: [{ PrivatePort: 7575, PublicPort: 7575, Type: "tcp" }] }, - { id: "b8c9d0e1f2a3", name: "pihole", host: "local", state: "running" as ContainerState, 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: [{ PrivatePort: 80, PublicPort: 80, Type: "tcp" }, { PrivatePort: 53, PublicPort: 53, Type: "udp" }] }, - { id: "c9d0e1f2a3b4", name: "nginx-proxy", host: "local", state: "running" as ContainerState, 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: [{ PrivatePort: 443, PublicPort: 443, Type: "tcp" }, { PrivatePort: 81, PublicPort: 81, Type: "tcp" }] }, - { id: "d0e1f2a3b4c5", name: "watchtower", host: "local", state: "running" as ContainerState, 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" as ContainerState, 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" as ContainerState, 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: [{ PrivatePort: 6767, PublicPort: 6767, Type: "tcp" }] }, +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({