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
This commit is contained in:
Thomas Camlong committed 2026-06-05 15:38:27 +02:00
1 parent 35bc50c12a
commit 0ad7b79187
9 files changed
+413 -83

No files matched your search

@@ -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.",
-1
View File
@@ -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";
+78 -22
View File
@@ -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 },
+65 -23
View File
@@ -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<CalendarEvent[]> {
return buildEventsForRange(start, end);
return await Promise.resolve(buildEventsForRange(start, end));
}
}
@@ -125,30 +125,40 @@ const mockItems: Omit<DownloadClientItem, "id" | "index">[] = [
export class DownloadClientMockService implements IDownloadClientIntegration {
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> {
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<void> {}
public async pauseQueueAsync(): Promise<void> {
await Promise.resolve();
}
public async pauseItemAsync(_item: DownloadClientItem): Promise<void> {}
public async pauseItemAsync(_item: DownloadClientItem): Promise<void> {
await Promise.resolve();
}
public async resumeQueueAsync(): Promise<void> {}
public async resumeQueueAsync(): Promise<void> {
await Promise.resolve();
}
public async resumeItemAsync(_item: DownloadClientItem): Promise<void> {}
public async resumeItemAsync(_item: DownloadClientItem): Promise<void> {
await Promise.resolve();
}
public async deleteItemAsync(_item: DownloadClientItem, _fromDisk: boolean): Promise<void> {}
public async deleteItemAsync(_item: DownloadClientItem, _fromDisk: boolean): Promise<void> {
await Promise.resolve();
}
}
@@ -2,7 +2,7 @@ import type { IMediaReleasesIntegration, MediaRelease } from "../../interfaces/m
export class MediaReleasesMockService implements IMediaReleasesIntegration {
public async getMediaReleasesAsync(): Promise<MediaRelease[]> {
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",
@@ -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<StreamSession[]> {
return sessions;
return await Promise.resolve(sessions);
}
}
@@ -3,12 +3,12 @@ import type { NetworkControllerSummary } from "../../types";
export class NetworkControllerSummaryMockService implements NetworkControllerSummaryIntegration {
public async getNetworkSummaryAsync(): Promise<NetworkControllerSummary> {
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 },
};
});
}
}
+151 -14
View File
@@ -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({