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,4 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [...baseConfig];

View File

@@ -0,0 +1 @@
export * from "./src";

View File

@@ -0,0 +1,38 @@
{
"name": "@homarr/definitions",
"version": "0.1.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts"
},
"typesVersions": {
"*": {
"*": [
"src/*"
]
}
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"postinstall": "tsx ./src/docs/codegen.ts",
"lint": "eslint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"fast-xml-parser": "^5.3.3",
"zod": "^4.2.1"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.39.2",
"tsx": "4.20.4",
"typescript": "^5.9.3"
}
}

View File

@@ -0,0 +1,17 @@
export const createDefinition = <const TKeys extends string[], TOptions extends { defaultValue: TKeys[number] } | void>(
values: TKeys,
options: TOptions,
) => ({
values,
defaultValue: options?.defaultValue as TOptions extends {
defaultValue: infer T;
}
? T
: undefined,
});
export type inferDefinitionType<TDefinition> = TDefinition extends {
values: readonly (infer T)[];
}
? T
: never;

View File

@@ -0,0 +1,2 @@
export const supportedAuthProviders = ["credentials", "oidc", "ldap"] as const;
export type SupportedAuthProvider = (typeof supportedAuthProviders)[number];

View File

@@ -0,0 +1,14 @@
import type { inferDefinitionType } from "./_definition";
import { createDefinition } from "./_definition";
export const backgroundImageAttachments = createDefinition(["fixed", "scroll"], { defaultValue: "fixed" });
export const backgroundImageRepeats = createDefinition(["repeat", "repeat-x", "repeat-y", "no-repeat"], {
defaultValue: "no-repeat",
});
export const backgroundImageSizes = createDefinition(["cover", "contain"], {
defaultValue: "cover",
});
export type BackgroundImageAttachment = inferDefinitionType<typeof backgroundImageAttachments>;
export type BackgroundImageRepeat = inferDefinitionType<typeof backgroundImageRepeats>;
export type BackgroundImageSize = inferDefinitionType<typeof backgroundImageSizes>;

View File

@@ -0,0 +1,2 @@
export const colorSchemeCookieKey = "homarr.color-scheme";
export const localeCookieKey = "homarr.locale";

View File

@@ -0,0 +1,11 @@
export const dockerContainerStates = [
"created",
"running",
"paused",
"restarting",
"exited",
"removing",
"dead",
] as const;
export type DockerContainerState = (typeof dockerContainerStates)[number];

View File

@@ -0,0 +1,75 @@
import fs from "node:fs/promises";
import path, { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { XMLParser } from "fast-xml-parser";
import { z } from "zod/v4";
import { createDocumentationLink } from "./index";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const removeCommonUrl = (url: string) => {
return url.replace("https://homarr.dev", "");
};
const sitemapSchema = z.object({
urlset: z.object({
url: z.array(
z.object({
loc: z.string(),
}),
),
}),
});
const fetchSitemapAsync = async () => {
const response = await fetch(createDocumentationLink("/sitemap.xml"));
return await response.text();
};
const parseXml = (sitemapXml: string) => {
const parser = new XMLParser();
const data: unknown = parser.parse(sitemapXml);
const result = sitemapSchema.safeParse(data);
if (!result.success) {
throw new Error("Invalid sitemap schema");
}
return result.data;
};
const mapSitemapXmlToPaths = (sitemapData: z.infer<typeof sitemapSchema>) => {
return sitemapData.urlset.url.map((url) => removeCommonUrl(url.loc));
};
const createSitemapPathType = (paths: string[]) => {
return "export type HomarrDocumentationPath =\n" + paths.map((path) => ` | "${path.replace(/\/$/, "")}"`).join("\n");
};
const updateSitemapTypeFileAsync = async (sitemapPathType: string) => {
const content =
"// This file is auto-generated by the codegen script\n" +
"// it uses the sitemap.xml to generate the HomarrDocumentationPath type\n" +
sitemapPathType +
";\n";
await fs.writeFile(path.join(__dirname, "homarr-docs-sitemap.ts"), content);
};
/**
* This script fetches the sitemap.xml and generates the HomarrDocumentationPath type
* which is used for typesafe documentation links
*/
// eslint-disable-next-line no-restricted-syntax
const main = async () => {
const sitemapXml = await fetchSitemapAsync();
const sitemapData = parseXml(sitemapXml);
const paths = mapSitemapXmlToPaths(sitemapData);
// Adding sitemap as it's not in the sitemap.xml and we need it for this file
paths.push("/sitemap.xml");
const sitemapPathType = createSitemapPathType(paths);
await updateSitemapTypeFileAsync(sitemapPathType);
};
void main();

View File

@@ -0,0 +1,233 @@
// This file is auto-generated by the codegen script
// it uses the sitemap.xml to generate the HomarrDocumentationPath type
export type HomarrDocumentationPath =
| "/about-us"
| "/blog"
| "/blog/2023/01/11/version0.11"
| "/blog/2023/04/16/version0.12-more-widgets"
| "/blog/2023/11/10/authentication"
| "/blog/2023/12/22/updated-documentation"
| "/blog/2024/09/23/version-1.0"
| "/blog/2024/12/17/open-beta-1.0"
| "/blog/2025/01/19/migration-guide-1.0"
| "/blog/2025/08/02/using-argus"
| "/blog/archive"
| "/blog/authors"
| "/blog/authors/ajnart"
| "/blog/authors/manuel-rw"
| "/blog/authors/meierschlumpf"
| "/blog/authors/tagashi"
| "/blog/authors/walkx"
| "/blog/documentation-migration"
| "/blog/tags"
| "/blog/tags/authentication"
| "/blog/tags/breaking-changes"
| "/blog/tags/contributions"
| "/blog/tags/design"
| "/blog/tags/dnd"
| "/blog/tags/docs"
| "/blog/tags/documentation"
| "/blog/tags/gridstack"
| "/blog/tags/homarr"
| "/blog/tags/migration"
| "/blog/tags/notepad"
| "/blog/tags/security"
| "/blog/tags/translations"
| "/blog/tags/update"
| "/blog/tags/version"
| "/blog/translations"
| "/search"
| "/docs/tags"
| "/docs/tags/active-directory"
| "/docs/tags/administration"
| "/docs/tags/advanced"
| "/docs/tags/analytics"
| "/docs/tags/api"
| "/docs/tags/apps"
| "/docs/tags/background"
| "/docs/tags/boards"
| "/docs/tags/bookmarks"
| "/docs/tags/caddy"
| "/docs/tags/certificates"
| "/docs/tags/code"
| "/docs/tags/community"
| "/docs/tags/configuration"
| "/docs/tags/connections"
| "/docs/tags/customization"
| "/docs/tags/data-sources"
| "/docs/tags/database"
| "/docs/tags/developer"
| "/docs/tags/development"
| "/docs/tags/docker"
| "/docs/tags/donation"
| "/docs/tags/edit-mode"
| "/docs/tags/env"
| "/docs/tags/environment-variables"
| "/docs/tags/getting-started"
| "/docs/tags/google"
| "/docs/tags/groups"
| "/docs/tags/help"
| "/docs/tags/icon-picker"
| "/docs/tags/icon-repositories"
| "/docs/tags/icons"
| "/docs/tags/installation"
| "/docs/tags/integration"
| "/docs/tags/integrations"
| "/docs/tags/interface"
| "/docs/tags/jobs"
| "/docs/tags/layout"
| "/docs/tags/ldap"
| "/docs/tags/management"
| "/docs/tags/media"
| "/docs/tags/oidc"
| "/docs/tags/open-collective"
| "/docs/tags/permissions"
| "/docs/tags/pgid"
| "/docs/tags/ping"
| "/docs/tags/programming"
| "/docs/tags/proxy"
| "/docs/tags/puid"
| "/docs/tags/redis"
| "/docs/tags/responsive"
| "/docs/tags/roles"
| "/docs/tags/search"
| "/docs/tags/search-engines"
| "/docs/tags/security"
| "/docs/tags/self-signed"
| "/docs/tags/seo"
| "/docs/tags/server"
| "/docs/tags/settings"
| "/docs/tags/sso"
| "/docs/tags/tasks"
| "/docs/tags/technical-documentation"
| "/docs/tags/traefik"
| "/docs/tags/translations"
| "/docs/tags/unraid"
| "/docs/tags/uploads"
| "/docs/tags/users"
| "/docs/tags/variables"
| "/docs/advanced/command-line"
| "/docs/advanced/command-line/fix-usernames"
| "/docs/advanced/command-line/password-recovery"
| "/docs/advanced/development/getting-started"
| "/docs/advanced/development/kubernetes"
| "/docs/advanced/environment-variables"
| "/docs/advanced/icons"
| "/docs/advanced/keyboard-shortcuts"
| "/docs/advanced/proxy"
| "/docs/advanced/running-as-different-user"
| "/docs/advanced/single-sign-on"
| "/docs/advanced/styling"
| "/docs/category/advanced"
| "/docs/category/community"
| "/docs/category/developer-guides"
| "/docs/category/getting-started"
| "/docs/category/installation"
| "/docs/category/installation-1"
| "/docs/category/integrations"
| "/docs/category/management"
| "/docs/category/widgets"
| "/docs/community/donate"
| "/docs/community/faq"
| "/docs/community/get-in-touch"
| "/docs/community/license"
| "/docs/community/translations"
| "/docs/getting-started"
| "/docs/getting-started/after-the-installation"
| "/docs/getting-started/glossary"
| "/docs/getting-started/installation/docker"
| "/docs/getting-started/installation/easy-panel"
| "/docs/getting-started/installation/helm"
| "/docs/getting-started/installation/home-assistant"
| "/docs/getting-started/installation/pika-pods"
| "/docs/getting-started/installation/portainer"
| "/docs/getting-started/installation/proxmox"
| "/docs/getting-started/installation/qnap"
| "/docs/getting-started/installation/railway"
| "/docs/getting-started/installation/runtipi"
| "/docs/getting-started/installation/saltbox"
| "/docs/getting-started/installation/source"
| "/docs/getting-started/installation/synology"
| "/docs/getting-started/installation/unraid"
| "/docs/integrations/adguard-home"
| "/docs/integrations/aria2"
| "/docs/integrations/codeberg"
| "/docs/integrations/dash-dot"
| "/docs/integrations/deluge"
| "/docs/integrations/docker-hub"
| "/docs/integrations/docker"
| "/docs/integrations/emby"
| "/docs/integrations/github-containerregistry"
| "/docs/integrations/github"
| "/docs/integrations/gitlab"
| "/docs/integrations/home-assistant"
| "/docs/integrations/ical"
| "/docs/integrations/jellyfin"
| "/docs/integrations/jellyseerr"
| "/docs/integrations/kubernetes"
| "/docs/integrations/lidarr"
| "/docs/integrations/linux-server-io"
| "/docs/integrations/nextcloud"
| "/docs/integrations/npm"
| "/docs/integrations/ntfy"
| "/docs/integrations/nzbget"
| "/docs/integrations/open-media-vault"
| "/docs/integrations/opnsense"
| "/docs/integrations/overseerr"
| "/docs/integrations/pi-hole"
| "/docs/integrations/plex"
| "/docs/integrations/prowlarr"
| "/docs/integrations/proxmox"
| "/docs/integrations/q-bittorent"
| "/docs/integrations/quay"
| "/docs/integrations/radarr"
| "/docs/integrations/readarr"
| "/docs/integrations/sabnzbd"
| "/docs/integrations/sonarr"
| "/docs/integrations/tdarr"
| "/docs/integrations/transmission"
| "/docs/integrations/truenas"
| "/docs/integrations/unifi-controller"
| "/docs/integrations/unraid"
| "/docs/management/api"
| "/docs/management/apps"
| "/docs/management/boards"
| "/docs/management/certificates"
| "/docs/management/integrations"
| "/docs/management/media"
| "/docs/management/search-engines"
| "/docs/management/settings"
| "/docs/management/tasks"
| "/docs/management/users"
| "/docs/widgets/app"
| "/docs/widgets/bookmarks"
| "/docs/widgets/calendar"
| "/docs/widgets/clock"
| "/docs/widgets/dns-hole-controls"
| "/docs/widgets/dns-hole-summary"
| "/docs/widgets/docker-containers"
| "/docs/widgets/downloads"
| "/docs/widgets/firewall"
| "/docs/widgets/health-monitoring"
| "/docs/widgets/iframe"
| "/docs/widgets/indexer-manager"
| "/docs/widgets/media-releases"
| "/docs/widgets/media-request-list"
| "/docs/widgets/media-request-stats"
| "/docs/widgets/media-server"
| "/docs/widgets/media-transcoding"
| "/docs/widgets/minecraft-server-status"
| "/docs/widgets/network-controller-status"
| "/docs/widgets/network-controller-summary"
| "/docs/widgets/notebook"
| "/docs/widgets/notifications"
| "/docs/widgets/releases"
| "/docs/widgets/rss-feed"
| "/docs/widgets/smart-home-entity-state"
| "/docs/widgets/smart-home-execute-automation"
| "/docs/widgets/stock-price"
| "/docs/widgets/system-resources"
| "/docs/widgets/video"
| "/docs/widgets/weather"
| ""
| "/sitemap.xml";

View File

@@ -0,0 +1,14 @@
import type { HomarrDocumentationPath } from "./homarr-docs-sitemap";
const documentationBaseUrl = "https://homarr.dev";
// Please use the method so the path can be checked!
export const createDocumentationLink = (
path: HomarrDocumentationPath,
hashTag?: `#${string}`,
queryParams?: Record<string, string>,
) => {
const url = `${documentationBaseUrl}${path}`;
const params = queryParams ? `?${new URLSearchParams(queryParams)}` : "";
return `${url}${params}${hashTag ?? ""}`;
};

View File

@@ -0,0 +1 @@
export const emptySuperJSON = '{"json": {}}';

View File

@@ -0,0 +1,2 @@
export const everyoneGroup = "everyone";
export const credentialsAdminGroup = "credentials-admin";

View File

@@ -0,0 +1,6 @@
export const hotkeys = {
toggleBoardEdit: "mod+e",
toggleColorScheme: "mod+j",
saveNotebook: "mod+s",
openSpotlight: "mod+k",
};

View File

@@ -0,0 +1,16 @@
export * from "./board";
export * from "./integration";
export * from "./section";
export * from "./widget";
export * from "./permissions";
export * from "./docker";
export * from "./kubernetes";
export * from "./auth";
export * from "./user";
export * from "./group";
export * from "./docs";
export * from "./cookie";
export * from "./search-engine";
export * from "./onboarding";
export * from "./emptysuperjson";
export * from "./hotkeys";

View File

@@ -0,0 +1,396 @@
import { objectKeys } from "@homarr/common";
import type { AtLeastOneOf } from "@homarr/common/types";
import { createDocumentationLink } from "./docs";
export const integrationSecretKindObject = {
apiKey: { isPublic: false, multiline: false },
username: { isPublic: true, multiline: false },
password: { isPublic: false, multiline: false },
tokenId: { isPublic: true, multiline: false },
realm: { isPublic: true, multiline: false },
personalAccessToken: { isPublic: false, multiline: false },
topic: { isPublic: true, multiline: false },
opnsenseApiKey: { isPublic: false, multiline: false },
opnsenseApiSecret: { isPublic: false, multiline: false },
url: { isPublic: false, multiline: false },
privateKey: { isPublic: false, multiline: true },
githubAppId: { isPublic: true, multiline: false },
githubInstallationId: { isPublic: true, multiline: false },
} satisfies Record<string, { isPublic: boolean; multiline: boolean }>;
export const integrationSecretKinds = objectKeys(integrationSecretKindObject);
interface integrationDefinition {
name: string;
iconUrl: string;
secretKinds: AtLeastOneOf<IntegrationSecretKind[]>; // at least one secret kind set is required
category: AtLeastOneOf<IntegrationCategory>;
documentationUrl: string | null;
defaultUrl?: string; // optional default URL for the integration
}
export const integrationDefs = {
sabNzbd: {
name: "SABnzbd",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/sabnzbd.svg",
category: ["downloadClient", "usenet"],
documentationUrl: createDocumentationLink("/docs/integrations/sabnzbd"),
},
nzbGet: {
name: "NZBGet",
secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/nzbget.svg",
category: ["downloadClient", "usenet"],
documentationUrl: createDocumentationLink("/docs/integrations/nzbget"),
},
deluge: {
name: "Deluge",
secretKinds: [["password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/deluge.svg",
category: ["downloadClient", "torrent"],
documentationUrl: createDocumentationLink("/docs/integrations/deluge"),
},
transmission: {
name: "Transmission",
secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/transmission.svg",
category: ["downloadClient", "torrent"],
documentationUrl: createDocumentationLink("/docs/integrations/transmission"),
},
qBittorrent: {
name: "qBittorrent",
secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/qbittorrent.svg",
category: ["downloadClient", "torrent"],
documentationUrl: createDocumentationLink("/docs/integrations/q-bittorent"),
},
aria2: {
name: "Aria2",
secretKinds: [[], ["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/PapirusDevelopmentTeam/papirus_icons@latest/src/system_downloads_3.svg",
category: ["downloadClient", "torrent", "miscellaneous"],
documentationUrl: createDocumentationLink("/docs/integrations/aria2"),
},
sonarr: {
name: "Sonarr",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/sonarr.svg",
category: ["calendar"],
documentationUrl: createDocumentationLink("/docs/integrations/sonarr"),
},
radarr: {
name: "Radarr",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/radarr.svg",
category: ["calendar"],
documentationUrl: createDocumentationLink("/docs/integrations/radarr"),
},
lidarr: {
name: "Lidarr",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/lidarr.svg",
category: ["calendar"],
documentationUrl: createDocumentationLink("/docs/integrations/lidarr"),
},
readarr: {
name: "Readarr",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/readarr.svg",
category: ["calendar"],
documentationUrl: createDocumentationLink("/docs/integrations/readarr"),
},
prowlarr: {
name: "Prowlarr",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/prowlarr.svg",
category: ["indexerManager"],
documentationUrl: createDocumentationLink("/docs/integrations/prowlarr"),
},
jellyfin: {
name: "Jellyfin",
secretKinds: [["username", "password"], ["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyfin.svg",
category: ["mediaService", "mediaRelease"],
documentationUrl: createDocumentationLink("/docs/integrations/jellyfin"),
},
emby: {
name: "Emby",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/emby.svg",
category: ["mediaService", "mediaRelease"],
documentationUrl: createDocumentationLink("/docs/integrations/emby"),
},
plex: {
name: "Plex",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/plex.svg",
category: ["mediaService", "mediaRelease"],
documentationUrl: createDocumentationLink("/docs/integrations/plex"),
},
jellyseerr: {
name: "Jellyseerr",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyseerr.svg",
category: ["mediaSearch", "mediaRequest", "search"],
documentationUrl: createDocumentationLink("/docs/integrations/jellyseerr"),
},
overseerr: {
name: "Overseerr",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/overseerr.svg",
category: ["mediaSearch", "mediaRequest", "search"],
documentationUrl: createDocumentationLink("/docs/integrations/overseerr"),
},
piHole: {
name: "Pi-hole",
secretKinds: [["apiKey"], []],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/pi-hole.svg",
category: ["dnsHole"],
documentationUrl: createDocumentationLink("/docs/integrations/pi-hole"),
},
adGuardHome: {
name: "AdGuard Home",
secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/adguard-home.svg",
category: ["dnsHole"],
documentationUrl: createDocumentationLink("/docs/integrations/adguard-home"),
},
homeAssistant: {
name: "Home Assistant",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/home-assistant.svg",
category: ["smartHomeServer", "calendar"],
documentationUrl: createDocumentationLink("/docs/integrations/home-assistant"),
},
openmediavault: {
name: "OpenMediaVault",
secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/openmediavault.svg",
category: ["healthMonitoring"],
documentationUrl: createDocumentationLink("/docs/integrations/open-media-vault"),
},
dashDot: {
name: "Dash.",
secretKinds: [[]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/dashdot.png",
category: ["healthMonitoring"],
documentationUrl: createDocumentationLink("/docs/integrations/dash-dot"),
},
tdarr: {
name: "Tdarr",
secretKinds: [[], ["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/tdarr.png",
category: ["mediaTranscoding"],
documentationUrl: createDocumentationLink("/docs/integrations/tdarr"),
},
proxmox: {
name: "Proxmox",
secretKinds: [["username", "tokenId", "apiKey", "realm"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/proxmox.svg",
category: ["healthMonitoring"],
documentationUrl: createDocumentationLink("/docs/integrations/proxmox"),
},
nextcloud: {
name: "Nextcloud",
secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/nextcloud.svg",
category: ["calendar"],
documentationUrl: createDocumentationLink("/docs/integrations/nextcloud"),
},
unifiController: {
name: "Unifi Controller",
secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/unifi.png",
category: ["networkController"],
documentationUrl: createDocumentationLink("/docs/integrations/unifi-controller"),
},
opnsense: {
name: "OPNsense",
secretKinds: [["opnsenseApiKey", "opnsenseApiSecret"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/opnsense.svg",
category: ["firewall"],
documentationUrl: createDocumentationLink("/docs/integrations/opnsense"),
},
github: {
name: "Github",
secretKinds: [[], ["personalAccessToken"], ["githubAppId", "githubInstallationId", "privateKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/github.svg",
category: ["releasesProvider"],
defaultUrl: "https://api.github.com",
documentationUrl: createDocumentationLink("/docs/integrations/github"),
},
dockerHub: {
name: "Docker Hub",
secretKinds: [[], ["username", "personalAccessToken"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/docker.svg",
category: ["releasesProvider"],
defaultUrl: "https://hub.docker.com",
documentationUrl: createDocumentationLink("/docs/integrations/docker-hub"),
},
gitlab: {
name: "Gitlab",
secretKinds: [[], ["personalAccessToken"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/gitlab.svg",
category: ["releasesProvider"],
defaultUrl: "https://gitlab.com",
documentationUrl: createDocumentationLink("/docs/integrations/gitlab"),
},
npm: {
name: "NPM",
secretKinds: [[]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/npm.svg",
category: ["releasesProvider"],
defaultUrl: "https://registry.npmjs.org",
documentationUrl: createDocumentationLink("/docs/integrations/npm"),
},
codeberg: {
name: "Codeberg",
secretKinds: [[], ["personalAccessToken"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/codeberg.svg",
category: ["releasesProvider"],
defaultUrl: "https://codeberg.org",
documentationUrl: createDocumentationLink("/docs/integrations/codeberg"),
},
linuxServerIO: {
name: "LinuxServer.io",
secretKinds: [[]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/linuxserver-io.svg",
category: ["releasesProvider"],
defaultUrl: "https://api.linuxserver.io",
documentationUrl: createDocumentationLink("/docs/integrations/linux-server-io"),
},
gitHubContainerRegistry: {
name: "GitHub Container Registry",
secretKinds: [[], ["personalAccessToken"], ["githubAppId", "githubInstallationId", "privateKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/github.svg",
category: ["releasesProvider"],
defaultUrl: "https://api.github.com",
documentationUrl: createDocumentationLink("/docs/integrations/github"),
},
quay: {
name: "Quay",
secretKinds: [[], ["personalAccessToken"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/quay.png",
category: ["releasesProvider"],
defaultUrl: "https://quay.io",
documentationUrl: createDocumentationLink("/docs/integrations/quay"),
},
ntfy: {
name: "ntfy",
secretKinds: [["topic"], ["topic", "apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/ntfy.svg",
category: ["notifications"],
documentationUrl: createDocumentationLink("/docs/integrations/ntfy"),
},
ical: {
name: "iCal",
secretKinds: [["url"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/ical.svg",
category: ["calendar"],
documentationUrl: createDocumentationLink("/docs/integrations/ical"),
},
truenas: {
name: "TrueNAS",
secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/truenas.svg",
category: ["healthMonitoring"],
documentationUrl: createDocumentationLink("/docs/integrations/truenas"),
},
unraid: {
name: "Unraid",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/unraid.svg",
category: ["healthMonitoring"],
documentationUrl: createDocumentationLink("/docs/integrations/unraid"),
},
// This integration only returns mock data, it is used during development (but can also be used in production by directly going to the create page)
mock: {
name: "Mock",
secretKinds: [[]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/vitest.svg",
category: [
"calendar",
"dnsHole",
"downloadClient",
"healthMonitoring",
"indexerManager",
"mediaRelease",
"mediaRequest",
"mediaService",
"mediaTranscoding",
"networkController",
"notifications",
"smartHomeServer",
],
documentationUrl: null,
},
} as const satisfies Record<string, integrationDefinition>;
export const integrationKinds = objectKeys(integrationDefs) as AtLeastOneOf<IntegrationKind>;
export const getIconUrl = (integration: IntegrationKind) => integrationDefs[integration].iconUrl;
export const getIntegrationName = (integration: IntegrationKind) => integrationDefs[integration].name;
export const getDefaultSecretKinds = (integration: IntegrationKind): IntegrationSecretKind[] =>
integrationDefs[integration].secretKinds[0];
export const getAllSecretKindOptions = (integration: IntegrationKind): AtLeastOneOf<IntegrationSecretKind[]> =>
integrationDefs[integration].secretKinds;
export const getIntegrationDefaultUrl = (integration: IntegrationKind) => {
const definition = integrationDefs[integration];
return "defaultUrl" in definition ? definition.defaultUrl : undefined;
};
/**
* Get all integration kinds that share a category, typed only by the kinds belonging to the category
* @param category Category to filter by, belonging to IntegrationCategory
* @returns Partial list of integration kinds
*/
export const getIntegrationKindsByCategory = <TCategory extends IntegrationCategory>(category: TCategory) => {
return objectKeys(integrationDefs).filter((integration) =>
integrationDefs[integration].category.some((defCategory) => defCategory === category),
) as AtLeastOneOf<IntegrationKindByCategory<TCategory>>;
};
/**
* Directly get the types of the list returned by getIntegrationKindsByCategory
*/
export type IntegrationKindByCategory<TCategory extends IntegrationCategory> = {
[Key in keyof typeof integrationDefs]: TCategory extends (typeof integrationDefs)[Key]["category"][number]
? Key
: never;
}[keyof typeof integrationDefs] extends infer U
? //Needed to simplify the type when using it
U
: never;
export type IntegrationSecretKind = keyof typeof integrationSecretKindObject;
export type IntegrationKind = keyof typeof integrationDefs;
export const integrationCategories = [
"dnsHole",
"mediaService",
"calendar",
"mediaSearch",
"mediaRelease",
"mediaRequest",
"downloadClient",
"usenet",
"torrent",
"miscellaneous",
"smartHomeServer",
"indexerManager",
"healthMonitoring",
"search",
"mediaTranscoding",
"networkController",
"releasesProvider",
"notifications",
"firewall",
] as const;
export type IntegrationCategory = (typeof integrationCategories)[number];

View File

@@ -0,0 +1,110 @@
export const kubernetesNodeStates = ["Ready", "NotReady"] as const;
export const kubernetesNamespaceStates = ["Active", "Terminating"] as const;
export const kubernetesResourceTypes = ["Reserved", "Used"] as const;
export const kubernetesCapacityTypes = ["Pods", "CPU", "Memory"] as const;
export const kubernetesLabelResourceTypes = [
"configmaps",
"pods",
"ingresses",
"namespaces",
"nodes",
"secrets",
"services",
"volumes",
] as const;
export type KubernetesNodeState = (typeof kubernetesNodeStates)[number];
export type KubernetesNamespaceState = (typeof kubernetesNamespaceStates)[number];
export type KubernetesResourceType = (typeof kubernetesResourceTypes)[number];
export type KubernetesCapacityType = (typeof kubernetesCapacityTypes)[number];
export type KubernetesLabelResourceType = (typeof kubernetesLabelResourceTypes)[number];
export interface KubernetesBaseResource {
name: string;
namespace?: string;
creationTimestamp?: Date;
}
export interface KubernetesVolume extends KubernetesBaseResource {
accessModes: string[];
storage: string;
storageClassName: string;
volumeMode: string;
volumeName: string;
status: string;
}
export interface KubernetesSecret extends KubernetesBaseResource {
type: string;
}
export interface KubernetesPod extends KubernetesBaseResource {
image?: string;
applicationType: string;
status: string;
}
export interface KubernetesService extends KubernetesBaseResource {
type: string;
ports?: string[];
targetPorts?: string[];
clusterIP: string;
}
export interface KubernetesIngressPath {
serviceName: string;
port: number;
}
export interface KubernetesIngressRuleAndPath {
host: string;
paths: KubernetesIngressPath[];
}
export interface KubernetesIngress extends KubernetesBaseResource {
className: string;
rulesAndPaths: KubernetesIngressRuleAndPath[];
}
export interface KubernetesNamespace extends KubernetesBaseResource {
status: KubernetesNamespaceState;
}
export interface KubernetesNode {
name: string;
status: KubernetesNodeState;
allocatableCpuPercentage: number;
allocatableRamPercentage: number;
podsCount: number;
operatingSystem?: string;
architecture?: string;
kubernetesVersion?: string;
creationTimestamp?: Date;
}
export interface KubernetesCluster {
name: string;
providers: string;
kubernetesVersion: string;
architecture: string;
nodeCount: number;
capacity: KubernetesCapacity[];
}
export interface KubernetesCapacity {
type: KubernetesCapacityType;
resourcesStats: KubernetesResourceStat[];
}
export interface KubernetesResourceStat {
percentageValue: number;
type: KubernetesResourceType;
capacityUnit?: string;
usedValue: number;
maxUsedValue: number;
}
export interface ClusterResourceCount {
label: string;
count: number;
}

View File

@@ -0,0 +1,2 @@
export const onboardingSteps = ["start", "import", "user", "group", "settings", "finish"] as const;
export type OnboardingStep = (typeof onboardingSteps)[number];

View File

@@ -0,0 +1,119 @@
import { objectEntries, objectKeys } from "@homarr/common";
/**
* Permissions for boards.
* view: Can view the board and its content. (e.g. see all items on the board, but not modify them)
* modify: Can modify the board, its content and visual settings. (e.g. move items, change the background)
* full: Can modify the board, its content, visual settings, access settings, delete, change the visibility and rename. (e.g. change the board name, delete the board, give access to other users)
*/
export const boardPermissions = ["view", "modify", "full"] as const;
export const boardPermissionsMap = {
view: "board-view-all",
modify: "board-modify-all",
full: "board-full-all",
} satisfies Record<BoardPermission, GroupPermissionKey>;
export type BoardPermission = (typeof boardPermissions)[number];
/**
* Permissions for integrations.
* use: Can select the integration for an item on the board. (e.g. select pi-hole for a widget)
* interact: Can interact with the integration. (e.g. enable / disable pi-hole)
* full: Can modify the integration. (e.g. change the pi-hole url, secrets and access settings)
*/
export const integrationPermissions = ["use", "interact", "full"] as const;
export const integrationPermissionsMap = {
use: "integration-use-all",
interact: "integration-interact-all",
full: "integration-full-all",
} satisfies Record<IntegrationPermission, GroupPermissionKey>;
export type IntegrationPermission = (typeof integrationPermissions)[number];
/**
* Global permissions that can be assigned to groups.
* The keys are generated through combining the key and all array items.
* For example "board-create" is a generated key
*/
export const groupPermissions = {
// Order is the same in the UI, inspired from order in navigation here
board: ["create", "view-all", "modify-all", "full-all"],
app: ["create", "use-all", "modify-all", "full-all"],
integration: ["create", "use-all", "interact-all", "full-all"],
"search-engine": ["create", "modify-all", "full-all"],
media: ["upload", "view-all", "full-all"],
other: ["view-logs"],
admin: true,
} as const;
/**
* In the following object is described how the permissions are related to each other.
* For example everybody with the permission "board-modify-all" also has the permission "board-view-all".
* Or admin has all permissions (board-full-all and integration-full-all which will resolve in an array of every permission).
*/
const groupPermissionParents = {
"board-modify-all": ["board-view-all"],
"board-full-all": ["board-modify-all", "board-create"],
"app-modify-all": ["app-create"],
"app-full-all": ["app-modify-all", "app-use-all"],
"integration-interact-all": ["integration-use-all"],
"integration-full-all": ["integration-interact-all", "integration-create"],
"search-engine-modify-all": ["search-engine-create"],
"search-engine-full-all": ["search-engine-modify-all"],
"media-full-all": ["media-upload", "media-view-all"],
admin: [
"board-full-all",
"app-full-all",
"integration-full-all",
"search-engine-full-all",
"media-full-all",
"other-view-logs",
],
} satisfies Partial<Record<GroupPermissionKey, GroupPermissionKey[]>>;
export const getPermissionsWithParents = (permissions: GroupPermissionKey[]): GroupPermissionKey[] => {
const res = permissions.map((permission) => {
return objectEntries(groupPermissionParents)
.filter(([_key, value]: [string, GroupPermissionKey[]]) => value.includes(permission))
.map(([key]) => getPermissionsWithParents([key]))
.flat();
});
return permissions.concat(res.flat());
};
const getPermissionsInner = (permissionSet: Set<GroupPermissionKey>, permissions: GroupPermissionKey[]) => {
permissions.forEach((permission) => {
const children = groupPermissionParents[permission as keyof typeof groupPermissionParents];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (children) {
getPermissionsInner(permissionSet, children);
}
permissionSet.add(permission);
});
};
export const getPermissionsWithChildren = (permissions: GroupPermissionKey[]) => {
const permissionSet = new Set<GroupPermissionKey>();
getPermissionsInner(permissionSet, permissions);
return Array.from(permissionSet);
};
type GroupPermissions = typeof groupPermissions;
export type GroupPermissionKey = {
[key in keyof GroupPermissions]: GroupPermissions[key] extends readonly string[]
? `${key}-${GroupPermissions[key][number]}`
: key;
}[keyof GroupPermissions];
export const groupPermissionKeys = objectKeys(groupPermissions).reduce((acc, key) => {
const item = groupPermissions[key];
if (typeof item !== "boolean") {
acc.push(...item.map((subKey) => `${key}-${subKey}` as GroupPermissionKey));
} else {
acc.push(key as GroupPermissionKey);
}
return acc;
}, [] as GroupPermissionKey[]);

View File

@@ -0,0 +1,2 @@
export const searchEngineTypes = ["generic", "fromIntegration"] as const;
export type SearchEngineType = (typeof searchEngineTypes)[number];

View File

@@ -0,0 +1,2 @@
export const sectionKinds = ["category", "empty", "dynamic"] as const;
export type SectionKind = (typeof sectionKinds)[number];

View File

@@ -0,0 +1,47 @@
/* eslint-disable no-restricted-syntax */
import { describe, expect, test } from "vitest";
import { createDocumentationLink } from "../docs";
import type { HomarrDocumentationPath } from "../docs/homarr-docs-sitemap";
describe("createDocumentationLink should generate correct URLs", () => {
test.each([
["/docs/getting-started", undefined, undefined, "https://homarr.dev/docs/getting-started"],
["/blog", undefined, undefined, "https://homarr.dev/blog"],
["/docs/widgets/weather", "#configuration", undefined, "https://homarr.dev/docs/widgets/weather#configuration"],
[
"/docs/advanced/environment-variables",
undefined,
{ lang: "en" },
"https://homarr.dev/docs/advanced/environment-variables?lang=en",
],
[
"/docs/widgets/bookmarks",
"#sorting",
{ lang: "fr", theme: "dark" },
"https://homarr.dev/docs/widgets/bookmarks?lang=fr&theme=dark#sorting",
],
] satisfies [HomarrDocumentationPath, `#${string}` | undefined, Record<string, string> | undefined, string][])(
"should create correct URL for path %s with hash %s and params %o",
(path, hashTag, queryParams, expected) => {
expect(createDocumentationLink(path, hashTag, queryParams)).toBe(expected);
},
);
});
describe("createDocumentationLink parameter validation", () => {
test("should work with only path parameter", () => {
const result = createDocumentationLink("/docs/getting-started");
expect(result).toBe("https://homarr.dev/docs/getting-started");
});
test("should work with path and hashtag", () => {
const result = createDocumentationLink("/docs/getting-started", "#installation");
expect(result).toBe("https://homarr.dev/docs/getting-started#installation");
});
test("should work with path and query params", () => {
const result = createDocumentationLink("/docs/getting-started", undefined, { version: "1.0" });
expect(result).toBe("https://homarr.dev/docs/getting-started?version=1.0");
});
});

View File

@@ -0,0 +1,14 @@
import { describe, expect, it } from "vitest";
import { objectEntries } from "@homarr/common";
import { integrationDefs } from "../integration";
describe("Icon url's of integrations should be valid and return 200", () => {
objectEntries(integrationDefs).forEach(([integration, { iconUrl }]) => {
it.concurrent(`should return 200 for ${integration}`, async () => {
const res = await fetch(iconUrl);
expect(res.status).toBe(200);
});
});
});

View File

@@ -0,0 +1,47 @@
import { describe, expect, test } from "vitest";
import type { GroupPermissionKey } from "../permissions";
import { getPermissionsWithChildren, getPermissionsWithParents } from "../permissions";
describe("getPermissionsWithParents should return the correct permissions", () => {
test.each([
[["board-view-all"], ["board-view-all", "board-modify-all", "board-full-all", "admin"]],
[["board-modify-all"], ["board-modify-all", "board-full-all", "admin"]],
[["board-create"], ["board-create", "board-full-all", "admin"]],
[["board-full-all"], ["board-full-all", "admin"]],
[["integration-use-all"], ["integration-use-all", "integration-interact-all", "integration-full-all", "admin"]],
[["integration-create"], ["integration-create", "integration-full-all", "admin"]],
[["integration-interact-all"], ["integration-interact-all", "integration-full-all", "admin"]],
[["integration-full-all"], ["integration-full-all", "admin"]],
[["admin"], ["admin"]],
] satisfies [GroupPermissionKey[], GroupPermissionKey[]][])("expect %s to return %s", (input, expectedOutput) => {
expect(getPermissionsWithParents(input)).toEqual(expect.arrayContaining(expectedOutput));
});
});
describe("getPermissionsWithChildren should return the correct permissions", () => {
test.each([
[["board-view-all"], ["board-view-all"]],
[["board-modify-all"], ["board-view-all", "board-modify-all"]],
[["board-create"], ["board-create"]],
[["board-full-all"], ["board-full-all", "board-modify-all", "board-view-all"]],
[["integration-use-all"], ["integration-use-all"]],
[["integration-create"], ["integration-create"]],
[["integration-interact-all"], ["integration-interact-all", "integration-use-all"]],
[["integration-full-all"], ["integration-full-all", "integration-interact-all", "integration-use-all"]],
[
["admin"],
[
"admin",
"board-full-all",
"board-modify-all",
"board-view-all",
"integration-full-all",
"integration-interact-all",
"integration-use-all",
],
],
] satisfies [GroupPermissionKey[], GroupPermissionKey[]][])("expect %s to return %s", (input, expectedOutput) => {
expect(getPermissionsWithChildren(input)).toEqual(expect.arrayContaining(expectedOutput));
});
});

View File

@@ -0,0 +1,2 @@
export const colorSchemes = ["light", "dark"] as const;
export type ColorScheme = (typeof colorSchemes)[number];

View File

@@ -0,0 +1,33 @@
export const widgetKinds = [
"clock",
"weather",
"app",
"iframe",
"video",
"notebook",
"dnsHoleSummary",
"dnsHoleControls",
"smartHome-entityState",
"smartHome-executeAutomation",
"stockPrice",
"mediaServer",
"calendar",
"downloads",
"mediaRequests-requestList",
"mediaRequests-requestStats",
"mediaTranscoding",
"minecraftServerStatus",
"networkControllerSummary",
"networkControllerStatus",
"rssFeed",
"bookmarks",
"indexerManager",
"healthMonitoring",
"releases",
"mediaReleases",
"dockerContainers",
"firewall",
"notifications",
"systemResources",
] as const;
export type WidgetKind = (typeof widgetKinds)[number];

View File

@@ -0,0 +1,8 @@
{
"extends": "@homarr/tsconfig/base.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["*.ts", "src"],
"exclude": ["node_modules"]
}