Replace entire codebase with homarr-labs/homarr

This commit is contained in:
Thomas Camlong
2026-01-15 21:54:44 +01:00
parent c5bc3b1559
commit 4fdd1fe351
4666 changed files with 409577 additions and 147434 deletions

View File

@@ -0,0 +1,20 @@
import type { CommonOldmarrWidgetDefinition } from "./common";
export type OldmarrBookmarkDefinition = CommonOldmarrWidgetDefinition<
"bookmark",
{
name: string;
items: {
id: string;
name: string;
href: string;
iconUrl: string;
openNewTab: boolean;
hideHostname: boolean;
hideIcon: boolean;
}[];
layout: "autoGrid" | "horizontal" | "vertical";
}
>;
export type BookmarkApp = OldmarrBookmarkDefinition["options"]["items"][number];

View File

@@ -0,0 +1,11 @@
import type { CommonOldmarrWidgetDefinition } from "./common";
export type OldmarrCalendarDefinition = CommonOldmarrWidgetDefinition<
"calendar",
{
hideWeekDays: boolean;
showUnmonitored: boolean;
radarrReleaseType: "inCinemas" | "physicalRelease" | "digitalRelease";
fontSize: "xs" | "sm" | "md" | "lg" | "xl";
}
>;

View File

@@ -0,0 +1,9 @@
import type { OldmarrWidgetKind } from "@homarr/old-schema";
export interface CommonOldmarrWidgetDefinition<
TId extends OldmarrWidgetKind,
TOptions extends Record<string, unknown>,
> {
id: TId;
options: TOptions;
}

View File

@@ -0,0 +1,53 @@
import type { CommonOldmarrWidgetDefinition } from "./common";
export type OldmarrDashdotDefinition = CommonOldmarrWidgetDefinition<
"dashdot",
{
dashName: string;
url: string;
usePercentages: boolean;
columns: number;
graphHeight: number;
graphsOrder: (
| {
key: "storage";
subValues: {
enabled: boolean;
compactView: boolean;
span: number;
multiView: boolean;
};
}
| {
key: "network";
subValues: {
enabled: boolean;
compactView: boolean;
span: number;
};
}
| {
key: "cpu";
subValues: {
enabled: boolean;
multiView: boolean;
span: number;
};
}
| {
key: "ram";
subValues: {
enabled: boolean;
span: number;
};
}
| {
key: "gpu";
subValues: {
enabled: boolean;
span: number;
};
}
)[];
}
>;

View File

@@ -0,0 +1,21 @@
import type { CommonOldmarrWidgetDefinition } from "./common";
export type OldmarrDateDefinition = CommonOldmarrWidgetDefinition<
"date",
{
timezone: string;
customTitle: string;
display24HourFormat: boolean;
dateFormat:
| "hide"
| "dddd, MMMM D"
| "dddd, D MMMM"
| "MMM D"
| "D MMM"
| "DD/MM/YYYY"
| "MM/DD/YYYY"
| "DD/MM"
| "MM/DD";
titleState: "none" | "city" | "both";
}
>;

View File

@@ -0,0 +1,4 @@
import type { CommonOldmarrWidgetDefinition } from "./common";
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export type OldmarrDlspeedDefinition = CommonOldmarrWidgetDefinition<"dlspeed", {}>;

View File

@@ -0,0 +1,8 @@
import type { CommonOldmarrWidgetDefinition } from "./common";
export type OldmarrDnsHoleControlsDefinition = CommonOldmarrWidgetDefinition<
"dns-hole-controls",
{
showToggleAllButtons: boolean;
}
>;

View File

@@ -0,0 +1,6 @@
import type { CommonOldmarrWidgetDefinition } from "./common";
export type OldmarrDnsHoleSummaryDefinition = CommonOldmarrWidgetDefinition<
"dns-hole-summary",
{ usePiHoleColors: boolean; layout: "column" | "row" | "grid" }
>;

View File

@@ -0,0 +1,21 @@
import type { CommonOldmarrWidgetDefinition } from "./common";
export type OldmarrHealthMonitoringDefinition = CommonOldmarrWidgetDefinition<
"health-monitoring",
{
fahrenheit: boolean;
cpu: boolean;
memory: boolean;
fileSystem: boolean;
defaultTabState: "system" | "cluster";
node: string;
defaultViewState: "storage" | "none" | "node" | "vm" | "lxc";
summary: boolean;
showNode: boolean;
showVM: boolean;
showLXCs: boolean;
showStorage: boolean;
sectionIndicatorColor: "all" | "any";
ignoreCert: boolean;
}
>;

View File

@@ -0,0 +1,16 @@
import type { CommonOldmarrWidgetDefinition } from "./common";
export type OldmarrIframeDefinition = CommonOldmarrWidgetDefinition<
"iframe",
{
embedUrl: string;
allowFullScreen: boolean;
allowScrolling: boolean;
allowTransparency: boolean;
allowPayment: boolean;
allowAutoPlay: boolean;
allowMicrophone: boolean;
allowCamera: boolean;
allowGeolocation: boolean;
}
>;

View File

@@ -0,0 +1,81 @@
import { objectEntries } from "@homarr/common";
import type { Inverse } from "@homarr/common/types";
import type { WidgetKind } from "@homarr/definitions";
import type { OldmarrBookmarkDefinition } from "./bookmark";
import type { OldmarrCalendarDefinition } from "./calendar";
import type { OldmarrDashdotDefinition } from "./dashdot";
import type { OldmarrDateDefinition } from "./date";
import type { OldmarrDlspeedDefinition } from "./dlspeed";
import type { OldmarrDnsHoleControlsDefinition } from "./dns-hole-controls";
import type { OldmarrDnsHoleSummaryDefinition } from "./dns-hole-summary";
import type { OldmarrHealthMonitoringDefinition } from "./health-monitoring";
import type { OldmarrIframeDefinition } from "./iframe";
import type { OldmarrIndexerManagerDefinition } from "./indexer-manager";
import type { OldmarrMediaRequestListDefinition } from "./media-requests-list";
import type { OldmarrMediaRequestStatsDefinition } from "./media-requests-stats";
import type { OldmarrMediaServerDefinition } from "./media-server";
import type { OldmarrMediaTranscodingDefinition } from "./media-transcoding";
import type { OldmarrNotebookDefinition } from "./notebook";
import type { OldmarrRssDefinition } from "./rss";
import type { OldmarrSmartHomeEntityStateDefinition } from "./smart-home-entity-state";
import type { OldmarrSmartHomeTriggerAutomationDefinition } from "./smart-home-trigger-automation";
import type { OldmarrTorrentStatusDefinition } from "./torrent-status";
import type { OldmarrUsenetDefinition } from "./usenet";
import type { OldmarrVideoStreamDefinition } from "./video-stream";
import type { OldmarrWeatherDefinition } from "./weather";
export type OldmarrWidgetDefinitions =
| OldmarrWeatherDefinition
| OldmarrDateDefinition
| OldmarrCalendarDefinition
| OldmarrIndexerManagerDefinition
| OldmarrDashdotDefinition
| OldmarrUsenetDefinition
| OldmarrTorrentStatusDefinition
| OldmarrDlspeedDefinition
| OldmarrRssDefinition
| OldmarrVideoStreamDefinition
| OldmarrIframeDefinition
| OldmarrMediaServerDefinition
| OldmarrMediaRequestListDefinition
| OldmarrMediaRequestStatsDefinition
| OldmarrDnsHoleSummaryDefinition
| OldmarrDnsHoleControlsDefinition
| OldmarrBookmarkDefinition
| OldmarrNotebookDefinition
| OldmarrSmartHomeEntityStateDefinition
| OldmarrSmartHomeTriggerAutomationDefinition
| OldmarrHealthMonitoringDefinition
| OldmarrMediaTranscodingDefinition;
export const widgetKindMapping = {
date: "clock",
calendar: "calendar",
"torrents-status": "downloads",
weather: "weather",
rss: "rssFeed",
"video-stream": "video",
iframe: "iframe",
"media-server": "mediaServer",
"dns-hole-summary": "dnsHoleSummary",
"dns-hole-controls": "dnsHoleControls",
notebook: "notebook",
"smart-home/entity-state": "smartHome-entityState",
"smart-home/trigger-automation": "smartHome-executeAutomation",
"media-requests-list": "mediaRequests-requestList",
"media-requests-stats": "mediaRequests-requestStats",
"indexer-manager": "indexerManager",
bookmark: "bookmarks",
"health-monitoring": "healthMonitoring",
dashdot: "healthMonitoring",
"media-transcoding": "mediaTranscoding",
dlspeed: null,
usenet: "downloads",
} satisfies Record<OldmarrWidgetDefinitions["id"], WidgetKind | null>;
export type WidgetMapping = typeof widgetKindMapping;
export type InversedWidgetMapping = Inverse<Omit<typeof widgetKindMapping, "dlspeed">>;
export const mapKind = (kind: OldmarrWidgetDefinitions["id"]): keyof InversedWidgetMapping | null =>
objectEntries(widgetKindMapping).find(([key]) => key === kind)?.[1] ?? null;

View File

@@ -0,0 +1,8 @@
import type { CommonOldmarrWidgetDefinition } from "./common";
export type OldmarrIndexerManagerDefinition = CommonOldmarrWidgetDefinition<
"indexer-manager",
{
openIndexerSiteInNewTab: boolean;
}
>;

View File

@@ -0,0 +1,9 @@
import type { CommonOldmarrWidgetDefinition } from "./common";
export type OldmarrMediaRequestListDefinition = CommonOldmarrWidgetDefinition<
"media-requests-list",
{
replaceLinksWithExternalHost: boolean;
openInNewTab: boolean;
}
>;

View File

@@ -0,0 +1,9 @@
import type { CommonOldmarrWidgetDefinition } from "./common";
export type OldmarrMediaRequestStatsDefinition = CommonOldmarrWidgetDefinition<
"media-requests-stats",
{
replaceLinksWithExternalHost: boolean;
openInNewTab: boolean;
}
>;

View File

@@ -0,0 +1,4 @@
import type { CommonOldmarrWidgetDefinition } from "./common";
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export type OldmarrMediaServerDefinition = CommonOldmarrWidgetDefinition<"media-server", {}>;

View File

@@ -0,0 +1,12 @@
import type { CommonOldmarrWidgetDefinition } from "./common";
export type OldmarrMediaTranscodingDefinition = CommonOldmarrWidgetDefinition<
"media-transcoding",
{
defaultView: "workers" | "queue" | "statistics";
showHealthCheck: boolean;
showHealthChecksInQueue: boolean;
queuePageSize: number;
showAppIcon: boolean;
}
>;

View File

@@ -0,0 +1,10 @@
import type { CommonOldmarrWidgetDefinition } from "./common";
export type OldmarrNotebookDefinition = CommonOldmarrWidgetDefinition<
"notebook",
{
showToolbar: boolean;
allowReadOnlyCheck: boolean;
content: string;
}
>;

View File

@@ -0,0 +1,15 @@
import type { CommonOldmarrWidgetDefinition } from "./common";
export type OldmarrRssDefinition = CommonOldmarrWidgetDefinition<
"rss",
{
rssFeedUrl: string[];
enableRtl: boolean;
refreshInterval: number;
dangerousAllowSanitizedItemContent: boolean;
textLinesClamp: number;
sortByPublishDateAscending: boolean;
sortPostsWithoutPublishDateToTheTop: boolean;
maximumAmountOfPosts: number;
}
>;

View File

@@ -0,0 +1,13 @@
import type { CommonOldmarrWidgetDefinition } from "./common";
export type OldmarrSmartHomeEntityStateDefinition = CommonOldmarrWidgetDefinition<
"smart-home/entity-state",
{
entityId: string;
appendUnit: boolean;
genericToggle: boolean;
automationId: string;
displayName: string;
displayFriendlyName: boolean;
}
>;

View File

@@ -0,0 +1,9 @@
import type { CommonOldmarrWidgetDefinition } from "./common";
export type OldmarrSmartHomeTriggerAutomationDefinition = CommonOldmarrWidgetDefinition<
"smart-home/trigger-automation",
{
automationId: string;
displayName: string;
}
>;

View File

@@ -0,0 +1,18 @@
import type { CommonOldmarrWidgetDefinition } from "./common";
export type OldmarrTorrentStatusDefinition = CommonOldmarrWidgetDefinition<
"torrents-status",
{
displayCompletedTorrents: boolean;
displayActiveTorrents: boolean;
speedLimitOfActiveTorrents: number;
displayStaleTorrents: boolean;
labelFilterIsWhitelist: boolean;
labelFilter: string[];
displayRatioWithFilter: boolean;
columnOrdering: boolean;
rowSorting: boolean;
columns: ("up" | "down" | "eta" | "progress")[];
nameColumnSize: number;
}
>;

View File

@@ -0,0 +1,4 @@
import type { CommonOldmarrWidgetDefinition } from "./common";
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export type OldmarrUsenetDefinition = CommonOldmarrWidgetDefinition<"usenet", {}>;

View File

@@ -0,0 +1,11 @@
import type { CommonOldmarrWidgetDefinition } from "./common";
export type OldmarrVideoStreamDefinition = CommonOldmarrWidgetDefinition<
"video-stream",
{
FeedUrl: string;
autoPlay: boolean;
muted: boolean;
controls: boolean;
}
>;

View File

@@ -0,0 +1,26 @@
import type { CommonOldmarrWidgetDefinition } from "./common";
export type OldmarrWeatherDefinition = CommonOldmarrWidgetDefinition<
"weather",
{
displayInFahrenheit: boolean;
displayCityName: boolean;
displayWeekly: boolean;
forecastDays: number;
location: {
name: string;
latitude: number;
longitude: number;
};
dateFormat:
| "hide"
| "dddd, MMMM D"
| "dddd, D MMMM"
| "MMM D"
| "D MMM"
| "DD/MM/YYYY"
| "MM/DD/YYYY"
| "DD/MM"
| "MM/DD";
}
>;

View File

@@ -0,0 +1,215 @@
import { objectEntries } from "@homarr/common";
import { createLogger } from "@homarr/core/infrastructure/logs";
import type { WidgetComponentProps } from "../../../widgets/src/definition";
import type { InversedWidgetMapping, OldmarrWidgetDefinitions, WidgetMapping } from "./definitions";
import { mapKind } from "./definitions";
const logger = createLogger({ module: "mapOptions" });
// This type enforces, that for all widget mappings there is a corresponding option mapping,
// each option of newmarr can be mapped from the value of the oldmarr options
type OptionMapping = {
[WidgetKey in keyof InversedWidgetMapping]: InversedWidgetMapping[WidgetKey] extends null
? null
: {
[OptionsKey in keyof WidgetComponentProps<WidgetKey>["options"]]: (
oldOptions: Extract<OldmarrWidgetDefinitions, { id: InversedWidgetMapping[WidgetKey] }>["options"],
appsMap: Map<string, string>,
) => WidgetComponentProps<WidgetKey>["options"][OptionsKey] | undefined;
};
};
const optionMapping: OptionMapping = {
"mediaRequests-requestList": {
linksTargetNewTab: (oldOptions) => oldOptions.openInNewTab,
},
"mediaRequests-requestStats": {},
bookmarks: {
title: (oldOptions) => oldOptions.name,
// It's safe to assume that the app exists, because the app is always created before the widget
// And the mapping is created in insertAppsAsync
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
items: (oldOptions, appsMap) => oldOptions.items.map((item) => appsMap.get(item.id)!),
layout: (oldOptions) => {
const mappedLayouts: Record<typeof oldOptions.layout, WidgetComponentProps<"bookmarks">["options"]["layout"]> = {
autoGrid: "grid",
horizontal: "row",
vertical: "column",
};
return mappedLayouts[oldOptions.layout];
},
hideTitle: () => undefined,
hideIcon: (oldOptions) => oldOptions.items.some((item) => item.hideIcon),
hideHostname: (oldOptions) => oldOptions.items.some((item) => item.hideHostname),
openNewTab: (oldOptions) => oldOptions.items.some((item) => item.openNewTab),
},
calendar: {
releaseType: (oldOptions) => [oldOptions.radarrReleaseType],
filterFutureMonths: () => undefined,
filterPastMonths: () => undefined,
showUnmonitored: ({ showUnmonitored }) => showUnmonitored,
},
clock: {
customTitle: (oldOptions) => oldOptions.customTitle,
customTitleToggle: (oldOptions) => oldOptions.titleState !== "none",
dateFormat: (oldOptions) => (oldOptions.dateFormat === "hide" ? undefined : oldOptions.dateFormat),
is24HourFormat: (oldOptions) => oldOptions.display24HourFormat,
showDate: (oldOptions) => oldOptions.dateFormat !== "hide",
showSeconds: () => undefined,
timezone: (oldOptions) => oldOptions.timezone,
useCustomTimezone: () => true,
customTimeFormat: () => undefined,
customDateFormat: () => undefined,
},
downloads: {
activeTorrentThreshold: (oldOptions) =>
"speedLimitOfActiveTorrents" in oldOptions ? oldOptions.speedLimitOfActiveTorrents : undefined,
applyFilterToRatio: (oldOptions) =>
"displayRatioWithFilter" in oldOptions ? oldOptions.displayRatioWithFilter : undefined,
categoryFilter: (oldOptions) => ("labelFilter" in oldOptions ? oldOptions.labelFilter : undefined),
filterIsWhitelist: (oldOptions) =>
"labelFilterIsWhitelist" in oldOptions ? oldOptions.labelFilterIsWhitelist : undefined,
enableRowSorting: (oldOptions) => ("rowSorting" in oldOptions ? oldOptions.rowSorting : undefined),
showCompletedTorrent: (oldOptions) =>
"displayCompletedTorrents" in oldOptions ? oldOptions.displayCompletedTorrents : undefined,
columns: () => ["integration", "name", "progress", "time", "actions"],
defaultSort: () => "type",
descendingDefaultSort: () => false,
showCompletedUsenet: () => true,
showCompletedHttp: () => true,
limitPerIntegration: () => undefined,
},
weather: {
forecastDayCount: (oldOptions) => oldOptions.forecastDays,
hasForecast: (oldOptions) => oldOptions.displayWeekly,
isFormatFahrenheit: (oldOptions) => oldOptions.displayInFahrenheit,
disableTemperatureDecimals: () => undefined,
showCurrentWindSpeed: () => undefined,
location: (oldOptions) => oldOptions.location,
showCity: (oldOptions) => oldOptions.displayCityName,
dateFormat: (oldOptions) => (oldOptions.dateFormat === "hide" ? undefined : oldOptions.dateFormat),
useImperialSpeed: () => undefined,
},
iframe: {
embedUrl: (oldOptions) => oldOptions.embedUrl,
allowAutoPlay: (oldOptions) => oldOptions.allowAutoPlay,
allowFullScreen: (oldOptions) => oldOptions.allowFullScreen,
allowPayment: (oldOptions) => oldOptions.allowPayment,
allowCamera: (oldOptions) => oldOptions.allowCamera,
allowMicrophone: (oldOptions) => oldOptions.allowMicrophone,
allowGeolocation: (oldOptions) => oldOptions.allowGeolocation,
allowScrolling: (oldOptions) => oldOptions.allowScrolling,
},
video: {
feedUrl: (oldOptions) => oldOptions.FeedUrl,
hasAutoPlay: (oldOptions) => oldOptions.autoPlay,
hasControls: (oldOptions) => oldOptions.controls,
isMuted: (oldOptions) => oldOptions.muted,
},
dnsHoleControls: {
showToggleAllButtons: (oldOptions) => oldOptions.showToggleAllButtons,
},
dnsHoleSummary: {
layout: (oldOptions) => oldOptions.layout,
usePiHoleColors: (oldOptions) => oldOptions.usePiHoleColors,
},
rssFeed: {
feedUrls: (oldOptions) => oldOptions.rssFeedUrl,
enableRtl: (oldOptions) => oldOptions.enableRtl,
maximumAmountPosts: (oldOptions) => oldOptions.maximumAmountOfPosts,
textLinesClamp: (oldOptions) => oldOptions.textLinesClamp,
hideDescription: () => undefined,
},
notebook: {
allowReadOnlyCheck: (oldOptions) => oldOptions.allowReadOnlyCheck,
content: (oldOptions) => oldOptions.content,
showToolbar: (oldOptions) => oldOptions.showToolbar,
},
"smartHome-entityState": {
entityId: (oldOptions) => oldOptions.entityId,
displayName: (oldOptions) => oldOptions.displayName,
clickable: () => undefined,
entityUnit: () => undefined,
},
"smartHome-executeAutomation": {
automationId: (oldOptions) => oldOptions.automationId,
displayName: (oldOptions) => oldOptions.displayName,
},
mediaServer: {
showOnlyPlaying: () => undefined,
},
indexerManager: {
openIndexerSiteInNewTab: (oldOptions) => oldOptions.openIndexerSiteInNewTab,
},
healthMonitoring: {
cpu: (oldOptions) =>
"cpu" in oldOptions
? oldOptions.cpu
: oldOptions.graphsOrder.some((graph) => graph.key === "cpu" && graph.subValues.enabled),
memory: (oldOptions) =>
"memory" in oldOptions
? oldOptions.memory
: oldOptions.graphsOrder.some((graph) => graph.key === "ram" && graph.subValues.enabled),
fahrenheit: (oldOptions) => ("fahrenheit" in oldOptions ? oldOptions.fahrenheit : undefined),
fileSystem: (oldOptions) =>
"fileSystem" in oldOptions
? oldOptions.fileSystem
: oldOptions.graphsOrder.some((graph) => graph.key === "storage" && graph.subValues.enabled),
defaultTab: (oldOptions) => ("defaultTabState" in oldOptions ? oldOptions.defaultTabState : undefined),
sectionIndicatorRequirement: (oldOptions) =>
"sectionIndicatorColor" in oldOptions ? oldOptions.sectionIndicatorColor : undefined,
showUptime: () => undefined,
visibleClusterSections: (oldOptions) => {
if (!("showNode" in oldOptions)) return undefined;
const oldKeys = {
showNode: "node" as const,
showLXCs: "lxc" as const,
showVM: "qemu" as const,
showStorage: "storage" as const,
} satisfies Partial<Record<keyof typeof oldOptions, string>>;
return objectEntries(oldKeys)
.filter(([key]) => oldOptions[key])
.map(([_, section]) => section);
},
},
mediaTranscoding: {
defaultView: (oldOptions) => oldOptions.defaultView,
queuePageSize: (oldOptions) => oldOptions.queuePageSize,
},
};
/**
* Maps the oldmarr options to the newmarr options
* @param type old widget type
* @param oldOptions oldmarr options for this item
* @param appsMap map of old app ids to new app ids
* @returns newmarr options for this item or null if the item did not exist in oldmarr
*/
export const mapOptions = <K extends OldmarrWidgetDefinitions["id"]>(
type: K,
oldOptions: Extract<OldmarrWidgetDefinitions, { id: K }>["options"],
appsMap: Map<string, string>,
) => {
logger.debug("Mapping old homarr options for widget", { type, options: JSON.stringify(oldOptions) });
const kind = mapKind(type);
if (!kind) {
return null;
}
const mapping = optionMapping[kind];
return objectEntries(mapping).reduce(
(acc, [key, value]: [string, (oldOptions: Record<string, unknown>, appsMap: Map<string, string>) => unknown]) => {
const newValue = value(oldOptions, appsMap);
logger.debug("Mapping old homarr option", { kind, key, newValue });
if (newValue !== undefined) {
acc[key] = newValue;
}
return acc;
},
{} as Record<string, unknown>,
) as WidgetComponentProps<Exclude<WidgetMapping[K], null>>["options"];
};