Skip to content

Commit 556f7cf

Browse files
SeDemalmanuel-rw
authored andcommitted
feat: Initial commit for health monitoring widget with graphs (Work in Progress)
1 parent 75ba3f2 commit 556f7cf

File tree

20 files changed

+666
-670
lines changed

20 files changed

+666
-670
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,51 @@
11
import { observable } from "@trpc/server/observable";
22

3+
import type { Modify } from "@homarr/common/types";
4+
import type { Integration } from "@homarr/db/schema/sqlite";
5+
import type { IntegrationKindByCategory } from "@homarr/definitions";
36
import type { HealthMonitoring } from "@homarr/integrations";
4-
import type { ProxmoxClusterInfo } from "@homarr/integrations/types";
5-
import { clusterInfoRequestHandler, systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring";
7+
import { systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring";
68

7-
import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration";
9+
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
810
import { createTRPCRouter, publicProcedure } from "../../trpc";
11+
import { z } from "zod";
912

1013
export const healthMonitoringRouter = createTRPCRouter({
11-
getSystemHealthStatus: publicProcedure
12-
.unstable_concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot"))
13-
.query(async ({ ctx }) => {
14+
getHealthStatus: publicProcedure
15+
.input(z.object({ pointCount: z.number().optional(), maxElements: z.number().optional() }))
16+
.unstable_concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "proxmox"))
17+
.query(async ({ input: { pointCount = 1, maxElements = 32 }, ctx }) => {
1418
return await Promise.all(
15-
ctx.integrations.map(async (integration) => {
16-
const innerHandler = systemInfoRequestHandler.handler(integration, {});
17-
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
19+
ctx.integrations.map(async (integrationWithSecrets) => {
20+
const innerHandler = systemInfoRequestHandler.handler(integrationWithSecrets, { maxElements, pointCount });
21+
const healthInfo = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
22+
23+
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
1824

1925
return {
20-
integrationId: integration.id,
21-
integrationName: integration.name,
22-
healthInfo: data,
23-
updatedAt: timestamp,
26+
integration,
27+
healthInfo,
2428
};
2529
}),
2630
);
2731
}),
28-
subscribeSystemHealthStatus: publicProcedure
29-
.unstable_concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot"))
30-
.subscription(({ ctx }) => {
31-
return observable<{ integrationId: string; healthInfo: HealthMonitoring; timestamp: Date }>((emit) => {
32+
33+
subscribeHealthStatus: publicProcedure
34+
.input(z.object({ maxElements: z.number().optional() }))
35+
.unstable_concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "proxmox"))
36+
.subscription(({ ctx, input: { maxElements = 32 } }) => {
37+
return observable<{
38+
integration: Modify<Integration, { kind: IntegrationKindByCategory<"healthMonitoring"> }>;
39+
healthInfo: { data: HealthMonitoring; timestamp: Date };
40+
}>((emit) => {
3241
const unsubscribes: (() => void)[] = [];
33-
for (const integration of ctx.integrations) {
34-
const innerHandler = systemInfoRequestHandler.handler(integration, {});
35-
const unsubscribe = innerHandler.subscribe((healthInfo) => {
42+
for (const integrationWithSecrets of ctx.integrations) {
43+
const innerHandler = systemInfoRequestHandler.handler(integrationWithSecrets, { maxElements, pointCount: 1 });
44+
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
45+
const unsubscribe = innerHandler.subscribe((data) => {
3646
emit.next({
37-
integrationId: integration.id,
38-
healthInfo,
39-
timestamp: new Date(),
47+
integration,
48+
healthInfo: { data, timestamp: new Date() },
4049
});
4150
});
4251
unsubscribes.push(unsubscribe);
@@ -48,26 +57,4 @@ export const healthMonitoringRouter = createTRPCRouter({
4857
};
4958
});
5059
}),
51-
getClusterHealthStatus: publicProcedure
52-
.unstable_concat(createOneIntegrationMiddleware("query", "proxmox"))
53-
.query(async ({ ctx }) => {
54-
const innerHandler = clusterInfoRequestHandler.handler(ctx.integration, {});
55-
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
56-
return data;
57-
}),
58-
subscribeClusterHealthStatus: publicProcedure
59-
.unstable_concat(createOneIntegrationMiddleware("query", "proxmox"))
60-
.subscription(({ ctx }) => {
61-
return observable<ProxmoxClusterInfo>((emit) => {
62-
const unsubscribes: (() => void)[] = [];
63-
const innerHandler = clusterInfoRequestHandler.handler(ctx.integration, {});
64-
const unsubscribe = innerHandler.subscribe((healthInfo) => {
65-
emit.next(healthInfo);
66-
});
67-
unsubscribes.push(unsubscribe);
68-
return () => {
69-
unsubscribe();
70-
};
71-
});
72-
}),
7360
});
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,14 @@
11
import { EVERY_5_SECONDS } from "@homarr/cron-jobs-core/expressions";
2-
import { clusterInfoRequestHandler, systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring";
2+
import { systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring";
33
import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler";
44

55
import { createCronJob } from "../../lib";
66

77
export const healthMonitoringJob = createCronJob("healthMonitoring", EVERY_5_SECONDS).withCallback(
8-
createRequestIntegrationJobHandler(
9-
(integration, itemOptions: Record<string, never>) => {
10-
const { kind } = integration;
11-
if (kind !== "proxmox") {
12-
return systemInfoRequestHandler.handler({ ...integration, kind }, itemOptions);
13-
}
14-
return clusterInfoRequestHandler.handler({ ...integration, kind }, itemOptions);
8+
createRequestIntegrationJobHandler(systemInfoRequestHandler.handler, {
9+
widgetKinds: ["healthMonitoring"],
10+
getInput: {
11+
healthMonitoring: (options) => ({ maxElements: Number(options.pointDensity), pointCount: 1 }),
1512
},
16-
{
17-
widgetKinds: ["healthMonitoring"],
18-
getInput: {
19-
healthMonitoring: () => ({}),
20-
},
21-
},
22-
),
13+
}),
2314
);

packages/definitions/src/integration.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,8 @@ export type IntegrationCategory =
213213
| "torrent"
214214
| "smartHomeServer"
215215
| "indexerManager"
216-
| "healthMonitoring"
217216
| "search"
218217
| "mediaTranscoding"
219-
| "networkController";
218+
| "networkController"
219+
| "healthMonitoring"
220+
| "tempNone";

packages/integrations/src/base/creator.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import type { Integration as DbIntegration } from "@homarr/db/schema";
44
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
55

66
import { AdGuardHomeIntegration } from "../adguard-home/adguard-home-integration";
7-
import { DashDotIntegration } from "../dashdot/dashdot-integration";
87
import { DelugeIntegration } from "../download-client/deluge/deluge-integration";
98
import { NzbGetIntegration } from "../download-client/nzbget/nzbget-integration";
109
import { QBitTorrentIntegration } from "../download-client/qbittorrent/qbittorrent-integration";
1110
import { SabnzbdIntegration } from "../download-client/sabnzbd/sabnzbd-integration";
1211
import { TransmissionIntegration } from "../download-client/transmission/transmission-integration";
1312
import { EmbyIntegration } from "../emby/emby-integration";
13+
import { DashDotIntegration } from "../health-monitoring/dashdot/dashdot-integration";
1414
import { HomeAssistantIntegration } from "../homeassistant/homeassistant-integration";
1515
import { JellyfinIntegration } from "../jellyfin/jellyfin-integration";
1616
import { JellyseerrIntegration } from "../jellyseerr/jellyseerr-integration";

packages/integrations/src/dashdot/dashdot-integration.ts

-149
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import type { z, ZodType } from "zod";
2+
3+
import type { HealthMonitoring } from "../../interfaces/health-monitoring/healt-monitoring-data";
4+
import { HealthMonitoringIntegration } from "../../interfaces/health-monitoring/health-monitoring-interface";
5+
import {
6+
configSchema,
7+
cpuLoadSchema,
8+
memoryLoadSchema,
9+
networkLoadSchema,
10+
serverInfoSchema,
11+
storageLoadSchema,
12+
} from "./dashdot-schema";
13+
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
14+
15+
export class DashDotIntegration extends HealthMonitoringIntegration {
16+
public async testConnectionAsync(): Promise<void> {
17+
const response = await fetchWithTrustedCertificatesAsync(this.url("/info"));
18+
await response.json();
19+
}
20+
21+
public async getSystemInfoAsync(): Promise<HealthMonitoring> {
22+
const { config } = await this.dashDotApiCallAsync("/config", configSchema);
23+
const info = await this.dashDotApiCallAsync("/info", serverInfoSchema);
24+
const cpuLoad = await this.dashDotApiCallAsync("/load/cpu", cpuLoadSchema);
25+
const memoryLoad = await this.dashDotApiCallAsync("/load/ram", memoryLoadSchema);
26+
const storageLoad = await this.dashDotApiCallAsync("/load/storage", storageLoadSchema);
27+
const networkLoad = await this.dashDotApiCallAsync("/load/network", networkLoadSchema);
28+
return {
29+
system: {
30+
name: config.override.os ?? info.os.distro,
31+
type: "single",
32+
version: info.os.release,
33+
uptime: info.os.uptime * 1000,
34+
},
35+
cpu: cpuLoad.map((cpu) => {
36+
return {
37+
id: `${cpu.core}`,
38+
name: `${cpu.core}`,
39+
temp: cpu.temp ? (config.use_imperial ? (cpu.temp - 32) / 1.8 : cpu.temp) : undefined,
40+
maxValue: 100,
41+
};
42+
}),
43+
memory: [
44+
{
45+
id: "unique",
46+
name: `${config.override.ram_brand ?? info.ram.layout[0].brand} - ${config.override.ram_type ?? info.ram.layout[0].type}`,
47+
maxValue: config.override.ram_size ?? info.ram.size,
48+
},
49+
],
50+
storage: info.storage.map((storage, index) => {
51+
return {
52+
name: storage.disks.map((disk) => config.override.storage_brands?.[disk.brand] ?? disk.brand).join(", "),
53+
used: storageLoad[index] ?? -1,
54+
size: storage.size,
55+
};
56+
}),
57+
network: [
58+
{
59+
id: "unique",
60+
name: config.use_network_interface,
61+
maxValue: ((config.override.network_interface_speed ?? info.network.interfaceSpeed) * 1000 ** 3) / 8,
62+
},
63+
],
64+
history: [
65+
{
66+
timestamp: Date.now(),
67+
cpu: cpuLoad.map(({ core, load }) => ({ id: `${core}`, value: load })),
68+
memory: [{ id: "unique", value: memoryLoad.load }],
69+
networkUp: [{ id: "unique", value: Math.round(networkLoad.up) }],
70+
networkDown: [{ id: "unique", value: Math.round(networkLoad.down) }],
71+
},
72+
],
73+
};
74+
}
75+
76+
private async dashDotApiCallAsync<T extends ZodType>(path: `/${string}`, schema: T): Promise<z.infer<T>> {
77+
const response = await fetchWithTrustedCertificatesAsync(this.url(path));
78+
return await schema.parseAsync(await response.json()) as Promise<z.infer<T>>;
79+
}
80+
}

0 commit comments

Comments
 (0)