refactor: streamline widget and integration architecture
This commit is contained in:
1 parent
ec295fe26e
commit
425cb653a1
243 files changed
+5722
-2255
No files matched your search
@@ -1,146 +1,198 @@
|
||||
import type { IntegrationKind } from "@homarr/definitions";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||
|
||||
import { AdGuardHomeIntegration } from "../adguard-home/adguard-home-integration";
|
||||
import { BeszelIntegration } from "../beszel/beszel-integration";
|
||||
import { BazarrIntegration } from "../bazarr/bazarr-integration";
|
||||
import { AnchorIntegration } from "../anchor/anchor-integration";
|
||||
import { AudiobookshelfIntegration } from "../audiobookshelf/audiobookshelf-integration";
|
||||
import { CoolifyIntegration } from "../coolify/coolify-integration";
|
||||
import { DashDotIntegration } from "../dashdot/dashdot-integration";
|
||||
import { Aria2Integration } from "../download-client/aria2/aria2-integration";
|
||||
import { DelugeIntegration } from "../download-client/deluge/deluge-integration";
|
||||
import { NzbGetIntegration } from "../download-client/nzbget/nzbget-integration";
|
||||
import { QBitTorrentIntegration } from "../download-client/qbittorrent/qbittorrent-integration";
|
||||
import { SabnzbdIntegration } from "../download-client/sabnzbd/sabnzbd-integration";
|
||||
import { SlskdIntegration } from "../download-client/slskd/slskd-integration";
|
||||
import { TransmissionIntegration } from "../download-client/transmission/transmission-integration";
|
||||
import { EmbyIntegration } from "../emby/emby-integration";
|
||||
import { GlancesIntegration } from "../glances/glances-integration";
|
||||
import { ArchiveTeamWarriorIntegration } from "../archive-team-warrior/archive-team-warrior-integration";
|
||||
import { GluetunIntegration } from "../gluetun/gluetun-integration";
|
||||
import { HomeAssistantIntegration } from "../homeassistant/homeassistant-integration";
|
||||
import { ICalIntegration } from "../ical/ical-integration";
|
||||
import { ImmichIntegration } from "../immich/immich-integration";
|
||||
import { JellyfinIntegration } from "../jellyfin/jellyfin-integration";
|
||||
import { JellyseerrIntegration } from "../jellyseerr/jellyseerr-integration";
|
||||
import { LidarrIntegration } from "../media-organizer/lidarr/lidarr-integration";
|
||||
import { RadarrIntegration } from "../media-organizer/radarr/radarr-integration";
|
||||
import { ReadarrIntegration } from "../media-organizer/readarr/readarr-integration";
|
||||
import { SonarrIntegration } from "../media-organizer/sonarr/sonarr-integration";
|
||||
import { TdarrIntegration } from "../media-transcoding/tdarr-integration";
|
||||
import { MockIntegration } from "../mock/mock-integration";
|
||||
import { NavidromeIntegration } from "../navidrome/navidrome-integration";
|
||||
import { NextcloudIntegration } from "../nextcloud/nextcloud.integration";
|
||||
import { GotifyIntegration } from "../gotify/gotify-integration";
|
||||
import { NTFYIntegration } from "../ntfy/ntfy-integration";
|
||||
import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-integration";
|
||||
import { OPNsenseIntegration } from "../opnsense/opnsense-integration";
|
||||
import { OverseerrIntegration } from "../overseerr/overseerr-integration";
|
||||
import { PaperlessNgxIntegration } from "../paperless-ngx/paperless-ngx-integration";
|
||||
import { PatchMonIntegration } from "../patchmon/patchmon-integration";
|
||||
import { PeaNutIntegration } from "../peanut/peanut-integration";
|
||||
import { createPiHoleIntegrationAsync } from "../pi-hole/pi-hole-integration-factory";
|
||||
import { createTechnitiumDnsIntegrationAsync } from "../technitium/technitium-integration-factory";
|
||||
import { PlexIntegration } from "../plex/plex-integration";
|
||||
import { ProwlarrIntegration } from "../prowlarr/prowlarr-integration";
|
||||
import { ProxmoxIntegration } from "../proxmox/proxmox-integration";
|
||||
import { SeerrIntegration } from "../seerr/seerr-integration";
|
||||
import { SpeedtestTrackerIntegration } from "../speedtest-tracker/speedtest-tracker-integration";
|
||||
import { TracearrIntegration } from "../tracearr/tracearr-integration";
|
||||
import { TraefikIntegration } from "../traefik/traefik-integration";
|
||||
import { SynologyIntegration } from "../synology/synology-integration";
|
||||
import { TrueNasIntegration } from "../truenas/truenas-integration";
|
||||
import { UmamiIntegration } from "../umami/umami-integration";
|
||||
import { UptimeKumaIntegration } from "../uptime-kuma/uptime-kuma-integration";
|
||||
import { UnifiControllerIntegration } from "../unifi-controller/unifi-controller-integration";
|
||||
import { UnraidIntegration } from "../unraid/unraid-integration";
|
||||
import { WudIntegration } from "../wud/wud-integration";
|
||||
import { generatedIntegrationCreatorLoaders } from "../generated/integration-creators";
|
||||
import type { Integration, IntegrationInput } from "./integration";
|
||||
|
||||
export const createIntegrationAsync = async <TKind extends keyof typeof integrationCreators>(
|
||||
const logger = createLogger({ module: "integrationFactory" });
|
||||
|
||||
type IntegrationCreator<TIntegration extends Integration = Integration> = (
|
||||
input: IntegrationInput,
|
||||
) => Promise<TIntegration>;
|
||||
|
||||
type IntegrationConstructor<TIntegration extends Integration = Integration> = new (
|
||||
input: IntegrationInput,
|
||||
) => TIntegration;
|
||||
|
||||
const createConstructorCreator =
|
||||
<TIntegration extends Integration>(
|
||||
IntegrationClass: IntegrationConstructor<TIntegration>,
|
||||
): IntegrationCreator<TIntegration> =>
|
||||
async (input) =>
|
||||
new IntegrationClass(input);
|
||||
|
||||
// Keep each import path explicit so Node and Turbopack can discover every lazy integration chunk.
|
||||
const integrationCreatorLoaders = {
|
||||
anchor: async () => createConstructorCreator((await import("../anchor/anchor-integration")).AnchorIntegration),
|
||||
piHole: async () => (await import("../pi-hole/pi-hole-integration-factory")).createPiHoleIntegrationAsync,
|
||||
adGuardHome: async () =>
|
||||
createConstructorCreator((await import("../adguard-home/adguard-home-integration")).AdGuardHomeIntegration),
|
||||
technitiumDns: async () =>
|
||||
(await import("../technitium/technitium-integration-factory")).createTechnitiumDnsIntegrationAsync,
|
||||
homeAssistant: async () =>
|
||||
createConstructorCreator((await import("../homeassistant/homeassistant-integration")).HomeAssistantIntegration),
|
||||
jellyfin: async () =>
|
||||
createConstructorCreator((await import("../jellyfin/jellyfin-integration")).JellyfinIntegration),
|
||||
plex: async () => createConstructorCreator((await import("../plex/plex-integration")).PlexIntegration),
|
||||
sonarr: async () =>
|
||||
createConstructorCreator((await import("../media-organizer/sonarr/sonarr-integration")).SonarrIntegration),
|
||||
radarr: async () =>
|
||||
createConstructorCreator((await import("../media-organizer/radarr/radarr-integration")).RadarrIntegration),
|
||||
sabNzbd: async () =>
|
||||
createConstructorCreator((await import("../download-client/sabnzbd/sabnzbd-integration")).SabnzbdIntegration),
|
||||
nzbGet: async () =>
|
||||
createConstructorCreator((await import("../download-client/nzbget/nzbget-integration")).NzbGetIntegration),
|
||||
qBittorrent: async () =>
|
||||
createConstructorCreator(
|
||||
(await import("../download-client/qbittorrent/qbittorrent-integration")).QBitTorrentIntegration,
|
||||
),
|
||||
deluge: async () =>
|
||||
createConstructorCreator((await import("../download-client/deluge/deluge-integration")).DelugeIntegration),
|
||||
transmission: async () =>
|
||||
createConstructorCreator(
|
||||
(await import("../download-client/transmission/transmission-integration")).TransmissionIntegration,
|
||||
),
|
||||
slskd: async () =>
|
||||
createConstructorCreator((await import("../download-client/slskd/slskd-integration")).SlskdIntegration),
|
||||
aria2: async () =>
|
||||
createConstructorCreator((await import("../download-client/aria2/aria2-integration")).Aria2Integration),
|
||||
jellyseerr: async () =>
|
||||
createConstructorCreator((await import("../jellyseerr/jellyseerr-integration")).JellyseerrIntegration),
|
||||
seerr: async () => createConstructorCreator((await import("../seerr/seerr-integration")).SeerrIntegration),
|
||||
overseerr: async () =>
|
||||
createConstructorCreator((await import("../overseerr/overseerr-integration")).OverseerrIntegration),
|
||||
prowlarr: async () =>
|
||||
createConstructorCreator((await import("../prowlarr/prowlarr-integration")).ProwlarrIntegration),
|
||||
openmediavault: async () =>
|
||||
createConstructorCreator((await import("../openmediavault/openmediavault-integration")).OpenMediaVaultIntegration),
|
||||
lidarr: async () =>
|
||||
createConstructorCreator((await import("../media-organizer/lidarr/lidarr-integration")).LidarrIntegration),
|
||||
readarr: async () =>
|
||||
createConstructorCreator((await import("../media-organizer/readarr/readarr-integration")).ReadarrIntegration),
|
||||
dashDot: async () => createConstructorCreator((await import("../dashdot/dashdot-integration")).DashDotIntegration),
|
||||
tdarr: async () =>
|
||||
createConstructorCreator((await import("../media-transcoding/tdarr-integration")).TdarrIntegration),
|
||||
proxmox: async () => createConstructorCreator((await import("../proxmox/proxmox-integration")).ProxmoxIntegration),
|
||||
emby: async () => createConstructorCreator((await import("../emby/emby-integration")).EmbyIntegration),
|
||||
nextcloud: async () =>
|
||||
createConstructorCreator((await import("../nextcloud/nextcloud.integration")).NextcloudIntegration),
|
||||
unifiController: async () =>
|
||||
createConstructorCreator(
|
||||
(await import("../unifi-controller/unifi-controller-integration")).UnifiControllerIntegration,
|
||||
),
|
||||
opnsense: async () =>
|
||||
createConstructorCreator((await import("../opnsense/opnsense-integration")).OPNsenseIntegration),
|
||||
ical: async () => createConstructorCreator((await import("../ical/ical-integration")).ICalIntegration),
|
||||
ntfy: async () => createConstructorCreator((await import("../ntfy/ntfy-integration")).NTFYIntegration),
|
||||
gotify: async () => createConstructorCreator((await import("../gotify/gotify-integration")).GotifyIntegration),
|
||||
mock: async () => createConstructorCreator((await import("../mock/mock-integration")).MockIntegration),
|
||||
truenas: async () => createConstructorCreator((await import("../truenas/truenas-integration")).TrueNasIntegration),
|
||||
synology: async () =>
|
||||
createConstructorCreator((await import("../synology/synology-integration")).SynologyIntegration),
|
||||
unraid: async () => createConstructorCreator((await import("../unraid/unraid-integration")).UnraidIntegration),
|
||||
coolify: async () => createConstructorCreator((await import("../coolify/coolify-integration")).CoolifyIntegration),
|
||||
tracearr: async () =>
|
||||
createConstructorCreator((await import("../tracearr/tracearr-integration")).TracearrIntegration),
|
||||
glances: async () => createConstructorCreator((await import("../glances/glances-integration")).GlancesIntegration),
|
||||
immich: async () => createConstructorCreator((await import("../immich/immich-integration")).ImmichIntegration),
|
||||
paperlessNgx: async () =>
|
||||
createConstructorCreator((await import("../paperless-ngx/paperless-ngx-integration")).PaperlessNgxIntegration),
|
||||
patchmon: async () =>
|
||||
createConstructorCreator((await import("../patchmon/patchmon-integration")).PatchMonIntegration),
|
||||
speedtestTracker: async () =>
|
||||
createConstructorCreator(
|
||||
(await import("../speedtest-tracker/speedtest-tracker-integration")).SpeedtestTrackerIntegration,
|
||||
),
|
||||
audiobookshelf: async () =>
|
||||
createConstructorCreator((await import("../audiobookshelf/audiobookshelf-integration")).AudiobookshelfIntegration),
|
||||
navidrome: async () =>
|
||||
createConstructorCreator((await import("../navidrome/navidrome-integration")).NavidromeIntegration),
|
||||
umami: async () => createConstructorCreator((await import("../umami/umami-integration")).UmamiIntegration),
|
||||
gluetun: async () => createConstructorCreator((await import("../gluetun/gluetun-integration")).GluetunIntegration),
|
||||
archiveTeamWarrior: async () =>
|
||||
createConstructorCreator(
|
||||
(await import("../archive-team-warrior/archive-team-warrior-integration")).ArchiveTeamWarriorIntegration,
|
||||
),
|
||||
uptimeKuma: async () =>
|
||||
createConstructorCreator((await import("../uptime-kuma/uptime-kuma-integration")).UptimeKumaIntegration),
|
||||
peaNut: async () => createConstructorCreator((await import("../peanut/peanut-integration")).PeaNutIntegration),
|
||||
...generatedIntegrationCreatorLoaders,
|
||||
bazarr: async () => createConstructorCreator((await import("../bazarr/bazarr-integration")).BazarrIntegration),
|
||||
traefik: async () => createConstructorCreator((await import("../traefik/traefik-integration")).TraefikIntegration),
|
||||
wud: async () => createConstructorCreator((await import("../wud/wud-integration")).WudIntegration),
|
||||
};
|
||||
|
||||
type AssertIntegrationCreatorLoaders<TLoaders extends Record<IntegrationKind, () => Promise<IntegrationCreator>>> =
|
||||
TLoaders;
|
||||
type AssertNoUnexpectedIntegrationKinds<TKind extends never> = TKind;
|
||||
type IntegrationCreatorLoadersCheck = AssertIntegrationCreatorLoaders<typeof integrationCreatorLoaders>;
|
||||
type UnexpectedIntegrationKindsCheck = AssertNoUnexpectedIntegrationKinds<
|
||||
Exclude<keyof typeof integrationCreatorLoaders, IntegrationKind>
|
||||
>;
|
||||
|
||||
type IntegrationCreatorLoaders = IntegrationCreatorLoadersCheck & Record<UnexpectedIntegrationKindsCheck, never>;
|
||||
type IntegrationCreatorOfKind<TKind extends keyof IntegrationCreatorLoaders> = Awaited<
|
||||
ReturnType<IntegrationCreatorLoaders[TKind]>
|
||||
>;
|
||||
type IntegrationInstanceOfKind<TKind extends keyof IntegrationCreatorLoaders> = Awaited<
|
||||
ReturnType<IntegrationCreatorOfKind<TKind>>
|
||||
>;
|
||||
|
||||
const integrationCreatorCache = new Map<IntegrationKind, Promise<IntegrationCreator>>();
|
||||
|
||||
const loadIntegrationCreatorAsync = async <TKind extends keyof IntegrationCreatorLoaders>(
|
||||
kind: TKind,
|
||||
): Promise<IntegrationCreatorOfKind<TKind>> => {
|
||||
const cachedCreator = integrationCreatorCache.get(kind);
|
||||
if (cachedCreator) {
|
||||
return (await cachedCreator) as IntegrationCreatorOfKind<TKind>;
|
||||
}
|
||||
|
||||
const startedAt = Date.now();
|
||||
logger.debug("Loading integration implementation", { integrationKind: kind });
|
||||
const creatorPromise = integrationCreatorLoaders[kind]();
|
||||
integrationCreatorCache.set(kind, creatorPromise);
|
||||
|
||||
try {
|
||||
const creator = (await creatorPromise) as IntegrationCreatorOfKind<TKind>;
|
||||
logger.debug("Integration implementation loaded", {
|
||||
integrationKind: kind,
|
||||
durationMs: Date.now() - startedAt,
|
||||
});
|
||||
return creator;
|
||||
} catch (error) {
|
||||
if (integrationCreatorCache.get(kind) === creatorPromise) {
|
||||
integrationCreatorCache.delete(kind);
|
||||
}
|
||||
logger.warn(
|
||||
new ErrorWithMetadata(
|
||||
"Integration implementation failed to load",
|
||||
{
|
||||
integrationKind: kind,
|
||||
durationMs: Date.now() - startedAt,
|
||||
},
|
||||
{ cause: error },
|
||||
),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const createIntegrationAsync = async <TKind extends keyof IntegrationCreatorLoaders>(
|
||||
integration: IntegrationInput & { kind: TKind },
|
||||
) => {
|
||||
if (!(integration.kind in integrationCreators)) {
|
||||
): Promise<IntegrationInstanceOfKind<TKind>> => {
|
||||
if (!Object.hasOwn(integrationCreatorLoaders, integration.kind)) {
|
||||
logger.warn("Integration client creation refused", {
|
||||
integrationId: integration.id,
|
||||
integrationKind: integration.kind,
|
||||
reason: "integration-kind-unregistered",
|
||||
});
|
||||
throw new Error(
|
||||
`Unknown integration kind ${integration.kind}. Did you forget to add it to the integration creator?`,
|
||||
);
|
||||
}
|
||||
|
||||
const creator = integrationCreators[integration.kind];
|
||||
|
||||
// factories are an array, to differentiate in js between class constructors and functions
|
||||
if (Array.isArray(creator)) {
|
||||
return (await creator[0](integration)) as IntegrationInstanceOfKind<TKind>;
|
||||
}
|
||||
|
||||
return new creator(integration) as IntegrationInstanceOfKind<TKind>;
|
||||
const creator = await loadIntegrationCreatorAsync(integration.kind);
|
||||
return (await creator(integration)) as IntegrationInstanceOfKind<TKind>;
|
||||
};
|
||||
|
||||
type IntegrationInstance = new (integration: IntegrationInput) => Integration;
|
||||
|
||||
// factories are an array, to differentiate in js between class constructors and functions
|
||||
export const integrationCreators = {
|
||||
anchor: AnchorIntegration,
|
||||
piHole: [createPiHoleIntegrationAsync],
|
||||
adGuardHome: AdGuardHomeIntegration,
|
||||
technitiumDns: [createTechnitiumDnsIntegrationAsync],
|
||||
homeAssistant: HomeAssistantIntegration,
|
||||
jellyfin: JellyfinIntegration,
|
||||
plex: PlexIntegration,
|
||||
sonarr: SonarrIntegration,
|
||||
radarr: RadarrIntegration,
|
||||
sabNzbd: SabnzbdIntegration,
|
||||
nzbGet: NzbGetIntegration,
|
||||
qBittorrent: QBitTorrentIntegration,
|
||||
deluge: DelugeIntegration,
|
||||
transmission: TransmissionIntegration,
|
||||
slskd: SlskdIntegration,
|
||||
aria2: Aria2Integration,
|
||||
jellyseerr: JellyseerrIntegration,
|
||||
seerr: SeerrIntegration,
|
||||
overseerr: OverseerrIntegration,
|
||||
prowlarr: ProwlarrIntegration,
|
||||
openmediavault: OpenMediaVaultIntegration,
|
||||
lidarr: LidarrIntegration,
|
||||
readarr: ReadarrIntegration,
|
||||
dashDot: DashDotIntegration,
|
||||
tdarr: TdarrIntegration,
|
||||
proxmox: ProxmoxIntegration,
|
||||
emby: EmbyIntegration,
|
||||
nextcloud: NextcloudIntegration,
|
||||
unifiController: UnifiControllerIntegration,
|
||||
opnsense: OPNsenseIntegration,
|
||||
ical: ICalIntegration,
|
||||
ntfy: NTFYIntegration,
|
||||
gotify: GotifyIntegration,
|
||||
mock: MockIntegration,
|
||||
truenas: TrueNasIntegration,
|
||||
synology: SynologyIntegration,
|
||||
unraid: UnraidIntegration,
|
||||
coolify: CoolifyIntegration,
|
||||
tracearr: TracearrIntegration,
|
||||
glances: GlancesIntegration,
|
||||
immich: ImmichIntegration,
|
||||
paperlessNgx: PaperlessNgxIntegration,
|
||||
patchmon: PatchMonIntegration,
|
||||
speedtestTracker: SpeedtestTrackerIntegration,
|
||||
audiobookshelf: AudiobookshelfIntegration,
|
||||
navidrome: NavidromeIntegration,
|
||||
umami: UmamiIntegration,
|
||||
gluetun: GluetunIntegration,
|
||||
archiveTeamWarrior: ArchiveTeamWarriorIntegration,
|
||||
uptimeKuma: UptimeKumaIntegration,
|
||||
peaNut: PeaNutIntegration,
|
||||
beszel: BeszelIntegration,
|
||||
bazarr: BazarrIntegration,
|
||||
traefik: TraefikIntegration,
|
||||
wud: WudIntegration,
|
||||
} satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>;
|
||||
|
||||
type IntegrationInstanceOfKind<TKind extends keyof typeof integrationCreators> = {
|
||||
[kind in TKind]: (typeof integrationCreators)[kind] extends [(input: IntegrationInput) => Promise<Integration>]
|
||||
? Awaited<ReturnType<(typeof integrationCreators)[kind][0]>>
|
||||
: (typeof integrationCreators)[kind] extends IntegrationInstance
|
||||
? InstanceType<(typeof integrationCreators)[kind]>
|
||||
: never;
|
||||
}[TKind];
|
||||
@@ -1,14 +1,18 @@
|
||||
import { isFunction } from "@homarr/common";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||
|
||||
import type { Integration } from "../integration";
|
||||
import type { IIntegrationErrorHandler } from "./handler";
|
||||
import { integrationFetchHttpErrorHandler } from "./http";
|
||||
import { IntegrationError } from "./integration-error";
|
||||
import { IntegrationUnknownError } from "./integration-unknown-error";
|
||||
import { IntegrationRequestError } from "./http/integration-request-error";
|
||||
import { IntegrationResponseError } from "./http/integration-response-error";
|
||||
import { integrationJsonParseErrorHandler, integrationZodParseErrorHandler } from "./parse";
|
||||
|
||||
const logger = createLogger({ module: "handleIntegrationErrors" });
|
||||
const loggedIntegrationErrors = new WeakSet<IntegrationError>();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-explicit-any
|
||||
type AbstractConstructor<T = {}> = abstract new (...args: any[]) => T;
|
||||
@@ -19,6 +23,62 @@ const defaultErrorHandlers: IIntegrationErrorHandler[] = [
|
||||
integrationFetchHttpErrorHandler,
|
||||
];
|
||||
|
||||
interface IntegrationOperationMetadata {
|
||||
operation: string;
|
||||
integrationId: string;
|
||||
integrationKind: string;
|
||||
integrationName: string;
|
||||
}
|
||||
|
||||
const safeErrorNameRegex = /^(?:Error|[A-Za-z][A-Za-z0-9]*(?:Error|Err))$/;
|
||||
|
||||
const createSafeErrorCause = (error: unknown, depth = 0): Error => {
|
||||
let options: ErrorOptions | undefined;
|
||||
if (depth < 5 && error instanceof Error && error.cause !== undefined) {
|
||||
options = { cause: createSafeErrorCause(error.cause, depth + 1) };
|
||||
}
|
||||
|
||||
const safeError = new Error("Integration operation failed", options);
|
||||
if (error instanceof Error && safeErrorNameRegex.test(error.name)) {
|
||||
safeError.name = error.name;
|
||||
}
|
||||
return safeError;
|
||||
};
|
||||
|
||||
const logFailure = (
|
||||
error: IntegrationError,
|
||||
failureType: "normalized" | "unknown",
|
||||
metadata: IntegrationOperationMetadata,
|
||||
durationMs: number,
|
||||
) => {
|
||||
if (loggedIntegrationErrors.has(error)) {
|
||||
return;
|
||||
}
|
||||
|
||||
loggedIntegrationErrors.add(error);
|
||||
const errorMetadata: Record<string, unknown> = { errorType: error.name };
|
||||
if (error instanceof IntegrationRequestError) {
|
||||
errorMetadata.requestType = error.cause.type;
|
||||
errorMetadata.requestReason = error.cause.reason;
|
||||
errorMetadata.requestCode = error.cause.code;
|
||||
}
|
||||
if (error instanceof IntegrationResponseError) {
|
||||
errorMetadata.statusCode = error.cause.statusCode;
|
||||
}
|
||||
logger.warn(
|
||||
new ErrorWithMetadata(
|
||||
"Integration operation failed",
|
||||
{
|
||||
...metadata,
|
||||
durationMs,
|
||||
failureType,
|
||||
...errorMetadata,
|
||||
},
|
||||
{ cause: createSafeErrorCause(error) },
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export const HandleIntegrationErrors = (errorHandlers: IIntegrationErrorHandler[]) => {
|
||||
const combinedErrorHandlers = [...defaultErrorHandlers, ...errorHandlers];
|
||||
return <T extends AbstractConstructor<Integration>>(IntegrationBaseClass: T): T => {
|
||||
@@ -44,8 +104,27 @@ export const HandleIntegrationErrors = (errorHandlers: IIntegrationErrorHandler[
|
||||
processedProperties.add(propertyKey);
|
||||
|
||||
const wrapped = (...methodArgs: unknown[]) => {
|
||||
const handleError = (error: unknown) => {
|
||||
const startedAt = Date.now();
|
||||
const operationMetadata: IntegrationOperationMetadata = {
|
||||
operation: propertyKey,
|
||||
integrationId: this.publicIntegration.id,
|
||||
integrationKind: this.integration.kind ?? "unknown",
|
||||
integrationName: this.publicIntegration.name,
|
||||
};
|
||||
|
||||
logger.debug("Integration operation started", operationMetadata);
|
||||
|
||||
const logCompletion = () => {
|
||||
logger.debug("Integration operation completed", {
|
||||
...operationMetadata,
|
||||
durationMs: Date.now() - startedAt,
|
||||
});
|
||||
};
|
||||
|
||||
const handleError = (error: unknown): never => {
|
||||
if (error instanceof IntegrationError) {
|
||||
const failureType = error instanceof IntegrationUnknownError ? "unknown" : "normalized";
|
||||
logFailure(error, failureType, operationMetadata, Date.now() - startedAt);
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -53,26 +132,29 @@ export const HandleIntegrationErrors = (errorHandlers: IIntegrationErrorHandler[
|
||||
const handledError = handler.handleError(error, this.publicIntegration);
|
||||
if (!handledError) continue;
|
||||
|
||||
logFailure(handledError, "normalized", operationMetadata, Date.now() - startedAt);
|
||||
throw handledError;
|
||||
}
|
||||
|
||||
// If the error was handled and should be thrown again, throw it
|
||||
logger.debug("Unhandled error in integration", {
|
||||
error: error instanceof Error ? `${error.name}: ${error.message}` : undefined,
|
||||
integrationName: this.publicIntegration.name,
|
||||
});
|
||||
throw new IntegrationUnknownError(this.publicIntegration, { cause: error });
|
||||
const unknownError = new IntegrationUnknownError(this.publicIntegration, { cause: error });
|
||||
logFailure(unknownError, "unknown", operationMetadata, Date.now() - startedAt);
|
||||
throw unknownError;
|
||||
};
|
||||
|
||||
try {
|
||||
const result = original.apply(this, methodArgs);
|
||||
|
||||
if (result instanceof Promise) {
|
||||
return result.catch((error: unknown) => {
|
||||
handleError(error);
|
||||
});
|
||||
return result.then(
|
||||
(value: unknown) => {
|
||||
logCompletion();
|
||||
return value;
|
||||
},
|
||||
(error: unknown) => handleError(error),
|
||||
);
|
||||
}
|
||||
|
||||
logCompletion();
|
||||
return result;
|
||||
} catch (error: unknown) {
|
||||
handleError(error);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { fetch as undiciFetch } from "undici";
|
||||
|
||||
import { removeTrailingSlash } from "@homarr/common";
|
||||
import { createAxiosCertificateInstanceAsync, createCertificateAgentAsync } from "@homarr/core/infrastructure/http";
|
||||
import type { IntegrationSecretKind } from "@homarr/definitions";
|
||||
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
|
||||
|
||||
import { HandleIntegrationErrors } from "./errors/decorator";
|
||||
import { TestConnectionError } from "./test-connection/test-connection-error";
|
||||
@@ -15,6 +15,7 @@ import type { IntegrationSecret } from "./types";
|
||||
|
||||
export interface IntegrationInput {
|
||||
id: string;
|
||||
kind?: IntegrationKind;
|
||||
name: string;
|
||||
url: string;
|
||||
externalUrl: string | null;
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 948 KiB |
@@ -0,0 +1,58 @@
|
||||
---
|
||||
title: "Beszel"
|
||||
description: "Beszel is a lightweight server monitoring platform with Docker stats, historical data, and alert functions."
|
||||
hide_title: true
|
||||
---
|
||||
|
||||
import { IntegrationHeader } from "@site/src/components/integrations/header";
|
||||
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
|
||||
import { AddingIntegration } from "@site/src/components/integrations/adding";
|
||||
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
|
||||
import { beszelIntegration } from ".";
|
||||
import { beszelSystemStatsWidget } from "@site/docs/widgets/beszel-system-stats";
|
||||
import { beszelSystemGridWidget } from "@site/docs/widgets/beszel-system-grid";
|
||||
import { beszelSystemTableWidget } from "@site/docs/widgets/beszel-system-table";
|
||||
import { beszelAlertsWidget } from "@site/docs/widgets/beszel-alerts";
|
||||
|
||||
import beszelDashboard from "./img/beszel-dashboard.png";
|
||||
|
||||
<IntegrationHeader integration={beszelIntegration} categories={["System Monitoring"]} />
|
||||
|
||||
Beszel monitors your servers in real time via lightweight agents, collecting CPU, memory, disk, network, GPU, temperature, and Docker container metrics. It stores historical data and supports configurable alerts.
|
||||
|
||||
### Screenshots
|
||||
|
||||
<img src={beszelDashboard} alt="Beszel dashboard overview showing all four widgets" />
|
||||
|
||||
### Widgets & Capabilities
|
||||
|
||||
<IntegrationCapabilites
|
||||
items={[
|
||||
{ widget: beszelSystemStatsWidget },
|
||||
{ widget: beszelSystemGridWidget },
|
||||
{ widget: beszelSystemTableWidget },
|
||||
{ widget: beszelAlertsWidget },
|
||||
]}
|
||||
/>
|
||||
|
||||
### Adding the integration
|
||||
|
||||
<AddingIntegration />
|
||||
|
||||
### Secrets
|
||||
|
||||
<IntegrationSecrets
|
||||
secrets={[
|
||||
{
|
||||
credentials: ["username", "password"],
|
||||
steps: [
|
||||
"Open your Beszel hub instance (default port 8090)",
|
||||
"Use the same credentials you use to log in to the Beszel web UI",
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
:::tip
|
||||
Beszel uses PocketBase under the hood. The default admin credentials are set during the initial Beszel setup.
|
||||
:::
|
||||
@@ -0,0 +1,7 @@
|
||||
export const beszelIntegration = {
|
||||
name: "Beszel",
|
||||
description:
|
||||
"Beszel is a lightweight server monitoring platform with Docker stats, historical data, and alert functions.",
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/beszel.svg",
|
||||
path: "../../integrations/beszel",
|
||||
} as const;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { defineIntegrationModule } from "@homarr/definitions";
|
||||
|
||||
export default defineIntegrationModule({
|
||||
kind: "beszel",
|
||||
name: "Beszel",
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/beszel.svg",
|
||||
secretKinds: [["username", "password"]],
|
||||
categories: ["beszel"],
|
||||
defaultPort: 8090,
|
||||
documentation: {
|
||||
slug: "beszel",
|
||||
sourceDirectory: "docs",
|
||||
},
|
||||
creator: {
|
||||
type: "constructor",
|
||||
module: "./beszel-integration",
|
||||
exportName: "BeszelIntegration",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export { createIntegrationAsync } from "./base/creator";
|
||||
@@ -0,0 +1,12 @@
|
||||
// This file is generated by pnpm integrations:sync. Do not edit it directly.
|
||||
|
||||
import type { Integration, IntegrationInput } from "../base/integration";
|
||||
|
||||
type GeneratedIntegrationCreator = (input: IntegrationInput) => Promise<Integration>;
|
||||
|
||||
export const generatedIntegrationCreatorLoaders = {
|
||||
beszel: async () => {
|
||||
const IntegrationClass = (await import("../beszel/beszel-integration")).BeszelIntegration;
|
||||
return async (input: IntegrationInput) => new IntegrationClass(input);
|
||||
},
|
||||
} satisfies Record<string, () => Promise<GeneratedIntegrationCreator>>;
|
||||
@@ -1,53 +1,6 @@
|
||||
// General integrations
|
||||
export { ArchiveTeamWarriorIntegration } from "./archive-team-warrior/archive-team-warrior-integration";
|
||||
export { WudIntegration } from "./wud/wud-integration";
|
||||
export { AdGuardHomeIntegration } from "./adguard-home/adguard-home-integration";
|
||||
export { TechnitiumDnsIntegration } from "./technitium/technitium-integration";
|
||||
export { AnchorIntegration } from "./anchor/anchor-integration";
|
||||
export { Aria2Integration } from "./download-client/aria2/aria2-integration";
|
||||
export { DelugeIntegration } from "./download-client/deluge/deluge-integration";
|
||||
export { NzbGetIntegration } from "./download-client/nzbget/nzbget-integration";
|
||||
export { QBitTorrentIntegration } from "./download-client/qbittorrent/qbittorrent-integration";
|
||||
export { SabnzbdIntegration } from "./download-client/sabnzbd/sabnzbd-integration";
|
||||
export { SlskdIntegration } from "./download-client/slskd/slskd-integration";
|
||||
export { TransmissionIntegration } from "./download-client/transmission/transmission-integration";
|
||||
export { HomeAssistantIntegration } from "./homeassistant/homeassistant-integration";
|
||||
export { JellyfinIntegration } from "./jellyfin/jellyfin-integration";
|
||||
export { JellyseerrIntegration } from "./jellyseerr/jellyseerr-integration";
|
||||
export { LidarrIntegration } from "./media-organizer/lidarr/lidarr-integration";
|
||||
export { RadarrIntegration } from "./media-organizer/radarr/radarr-integration";
|
||||
export { ReadarrIntegration } from "./media-organizer/readarr/readarr-integration";
|
||||
export { SonarrIntegration } from "./media-organizer/sonarr/sonarr-integration";
|
||||
export { NextcloudIntegration } from "./nextcloud/nextcloud.integration";
|
||||
export { GotifyIntegration } from "./gotify/gotify-integration";
|
||||
export { NTFYIntegration } from "./ntfy/ntfy-integration";
|
||||
export { OpenMediaVaultIntegration } from "./openmediavault/openmediavault-integration";
|
||||
export { GlancesIntegration } from "./glances/glances-integration";
|
||||
export { OverseerrIntegration } from "./overseerr/overseerr-integration";
|
||||
export { SeerrIntegration } from "./seerr/seerr-integration";
|
||||
export { PiHoleIntegrationV5 } from "./pi-hole/v5/pi-hole-integration-v5";
|
||||
export { PiHoleIntegrationV6 } from "./pi-hole/v6/pi-hole-integration-v6";
|
||||
export { PlexIntegration } from "./plex/plex-integration";
|
||||
export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration";
|
||||
export { TrueNasIntegration } from "./truenas/truenas-integration";
|
||||
export { SynologyIntegration } from "./synology/synology-integration";
|
||||
export { UnraidIntegration } from "./unraid/unraid-integration";
|
||||
export { OPNsenseIntegration } from "./opnsense/opnsense-integration";
|
||||
export { ICalIntegration } from "./ical/ical-integration";
|
||||
export { CoolifyIntegration } from "./coolify/coolify-integration";
|
||||
export { ImmichIntegration } from "./immich/immich-integration";
|
||||
export { TracearrIntegration } from "./tracearr/tracearr-integration";
|
||||
export { SpeedtestTrackerIntegration } from "./speedtest-tracker/speedtest-tracker-integration";
|
||||
export { AudiobookshelfIntegration } from "./audiobookshelf/audiobookshelf-integration";
|
||||
export { NavidromeIntegration } from "./navidrome/navidrome-integration";
|
||||
export { UptimeKumaIntegration } from "./uptime-kuma/uptime-kuma-integration";
|
||||
export { UmamiIntegration } from "./umami/umami-integration";
|
||||
export { PeaNutIntegration } from "./peanut/peanut-integration";
|
||||
export { BazarrIntegration } from "./bazarr/bazarr-integration";
|
||||
export { TraefikIntegration } from "./traefik/traefik-integration";
|
||||
|
||||
// Types
|
||||
// Type-only compatibility surface. Runtime capabilities have explicit subpaths.
|
||||
export type { IntegrationInput } from "./base/integration";
|
||||
export type { PiHoleIntegrationV6 } from "./pi-hole/v6/pi-hole-integration-v6";
|
||||
export type { DownloadClientJobsAndStatus } from "./interfaces/downloads/download-client-data";
|
||||
export type { ExtendedDownloadClientItem } from "./interfaces/downloads/download-client-items";
|
||||
export type { ExtendedClientStatus } from "./interfaces/downloads/download-client-status";
|
||||
@@ -59,8 +12,11 @@ export type {
|
||||
FirewallMemorySummary,
|
||||
} from "./interfaces/firewall-summary/firewall-summary-types";
|
||||
export type { SystemHealthMonitoring } from "./interfaces/health-monitoring/health-monitoring-types";
|
||||
export { UpstreamMediaRequestStatus } from "./interfaces/media-requests/media-request-types";
|
||||
export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request-types";
|
||||
export type {
|
||||
MediaRequestList,
|
||||
MediaRequestStats,
|
||||
UpstreamMediaRequestStatus,
|
||||
} from "./interfaces/media-requests/media-request-types";
|
||||
export type { StreamSession } from "./interfaces/media-server/media-server-types";
|
||||
export type { EntityState } from "./interfaces/smart-home/smart-home-types";
|
||||
export type {
|
||||
@@ -95,12 +51,4 @@ export type {
|
||||
} from "./archive-team-warrior/archive-team-warrior-types";
|
||||
export type { WudStats, WudContainerUpdate } from "./wud/wud-types";
|
||||
|
||||
// Schemas
|
||||
export { anchorNotesListInputSchema } from "./anchor/anchor-types";
|
||||
export { anchorNoteUpdateInputSchema } from "./anchor/anchor-types";
|
||||
export { downloadClientItemSchema } from "./interfaces/downloads/download-client-items";
|
||||
|
||||
// Helpers
|
||||
export { createIntegrationAsync } from "./base/creator";
|
||||
export { simulateResponseContractAsync } from "./base/response-contract";
|
||||
export type { ResponseContractFixture, ResponseContractParser } from "./base/response-contract";
|
||||
Reference in new issue
Block a user