Skip to content

Commit 2fb30ba

Browse files
committed
feat: Initial commit for health monitoring widget with graphs (Work in Progress)
1 parent 99bb680 commit 2fb30ba

File tree

21 files changed

+825
-583
lines changed

21 files changed

+825
-583
lines changed

packages/api/src/router/widgets/health-monitoring.ts

+25-16
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,52 @@
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 { getIntegrationKindsByCategory } from "@homarr/definitions";
47
import type { HealthMonitoring } from "@homarr/integrations";
58
import { systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring";
9+
import { z } from "@homarr/validation";
610

711
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
812
import { createTRPCRouter, publicProcedure } from "../../trpc";
913

1014
export const healthMonitoringRouter = createTRPCRouter({
1115
getHealthStatus: publicProcedure
16+
.input(z.object({ pointCount: z.number().optional(), maxElements: z.number().optional() }))
1217
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("healthMonitoring")))
13-
.query(async ({ ctx }) => {
18+
.query(async ({ input: { pointCount = 1, maxElements = 32 }, ctx }) => {
1419
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 });
20+
ctx.integrations.map(async (integrationWithSecrets) => {
21+
const innerHandler = systemInfoRequestHandler.handler(integrationWithSecrets, { maxElements, pointCount });
22+
const healthInfo = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
23+
24+
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
1825

1926
return {
20-
integrationId: integration.id,
21-
integrationName: integration.name,
22-
healthInfo: data,
23-
updatedAt: timestamp,
27+
integration,
28+
healthInfo,
2429
};
2530
}),
2631
);
2732
}),
2833

2934
subscribeHealthStatus: publicProcedure
35+
.input(z.object({ maxElements: z.number().optional() }))
3036
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("healthMonitoring")))
31-
.subscription(({ ctx }) => {
32-
return observable<{ integrationId: string; healthInfo: HealthMonitoring; timestamp: Date }>((emit) => {
37+
.subscription(({ ctx, input: { maxElements = 32 } }) => {
38+
return observable<{
39+
integration: Modify<Integration, { kind: IntegrationKindByCategory<"healthMonitoring"> }>;
40+
healthInfo: { data: HealthMonitoring; timestamp: Date };
41+
}>((emit) => {
3342
const unsubscribes: (() => void)[] = [];
34-
for (const integration of ctx.integrations) {
35-
const innerHandler = systemInfoRequestHandler.handler(integration, {});
36-
const unsubscribe = innerHandler.subscribe((healthInfo) => {
43+
for (const integrationWithSecrets of ctx.integrations) {
44+
const innerHandler = systemInfoRequestHandler.handler(integrationWithSecrets, { maxElements, pointCount: 1 });
45+
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
46+
const unsubscribe = innerHandler.subscribe((data) => {
3747
emit.next({
38-
integrationId: integration.id,
39-
healthInfo,
40-
timestamp: new Date(),
48+
integration,
49+
healthInfo: { data, timestamp: new Date() },
4150
});
4251
});
4352
unsubscribes.push(unsubscribe);

packages/cron-jobs/src/jobs/integrations/health-monitoring.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export const healthMonitoringJob = createCronJob("healthMonitoring", EVERY_5_SEC
88
createRequestIntegrationJobHandler(systemInfoRequestHandler.handler, {
99
widgetKinds: ["healthMonitoring"],
1010
getInput: {
11-
healthMonitoring: () => ({}),
11+
healthMonitoring: (options) => ({ maxElements: Number(options.pointDensity), pointCount: 1 }),
1212
},
1313
}),
1414
);

packages/definitions/src/docs/homarr-docs-sitemap.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,13 @@ export type HomarrDocumentationPath =
4141
| "/docs/tags/advanced"
4242
| "/docs/tags/analytics"
4343
| "/docs/tags/api"
44+
| "/docs/tags/apps"
4445
| "/docs/tags/banner"
4546
| "/docs/tags/blocking"
4647
| "/docs/tags/board"
4748
| "/docs/tags/boards"
4849
| "/docs/tags/bookmark"
50+
| "/docs/tags/bookmarks"
4951
| "/docs/tags/caddy"
5052
| "/docs/tags/checklist"
5153
| "/docs/tags/code"
@@ -91,6 +93,7 @@ export type HomarrDocumentationPath =
9193
| "/docs/tags/overseerr"
9294
| "/docs/tags/permissions"
9395
| "/docs/tags/pi-hole"
96+
| "/docs/tags/ping"
9497
| "/docs/tags/preferences"
9598
| "/docs/tags/programming"
9699
| "/docs/tags/proxmox"
@@ -128,24 +131,26 @@ export type HomarrDocumentationPath =
128131
| "/docs/advanced/customizations/user-preferences"
129132
| "/docs/advanced/sso"
130133
| "/docs/category/advanced"
134+
| "/docs/category/developer-guide"
131135
| "/docs/category/getting-started"
132136
| "/docs/category/installation"
133137
| "/docs/category/installation-1"
134138
| "/docs/category/integrations"
135139
| "/docs/category/management"
136140
| "/docs/category/more"
137141
| "/docs/category/widgets"
138-
| "/docs/community/developer-guides"
139142
| "/docs/community/donate"
140143
| "/docs/community/faq"
141144
| "/docs/community/get-in-touch"
142145
| "/docs/community/license"
143146
| "/docs/community/translations"
147+
| "/docs/development/getting-started"
144148
| "/docs/getting-started"
145149
| "/docs/getting-started/after-the-installation"
146150
| "/docs/getting-started/glossary"
147151
| "/docs/getting-started/installation/docker"
148152
| "/docs/getting-started/installation/easy-panel"
153+
| "/docs/getting-started/installation/helm"
149154
| "/docs/getting-started/installation/home-assistant"
150155
| "/docs/getting-started/installation/kubernetes"
151156
| "/docs/getting-started/installation/portainer"
@@ -164,6 +169,7 @@ export type HomarrDocumentationPath =
164169
| "/docs/integrations/torrent"
165170
| "/docs/integrations/usenet"
166171
| "/docs/management/api"
172+
| "/docs/management/apps"
167173
| "/docs/management/boards"
168174
| "/docs/management/integrations"
169175
| "/docs/management/search-engines"

packages/definitions/src/integration.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ export const integrationDefs = {
141141
name: "OpenMediaVault",
142142
secretKinds: [["username", "password"]],
143143
iconUrl: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/openmediavault.png",
144-
category: ["healthMonitoring"],
144+
category: ["tempNone"],
145145
supportsSearch: false,
146146
},
147147
dashDot: {
@@ -217,4 +217,5 @@ export type IntegrationCategory =
217217
| "torrent"
218218
| "smartHomeServer"
219219
| "indexerManager"
220-
| "healthMonitoring";
220+
| "healthMonitoring"
221+
| "tempNone";

packages/integrations/src/base/creator.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import type { Integration as DbIntegration } from "@homarr/db/schema/sqlite";
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";
12+
import { DashDotIntegration } from "../health-monitoring/dashdot/dashdot-integration";
1313
import { HomeAssistantIntegration } from "../homeassistant/homeassistant-integration";
1414
import { JellyfinIntegration } from "../jellyfin/jellyfin-integration";
1515
import { JellyseerrIntegration } from "../jellyseerr/jellyseerr-integration";

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

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

0 commit comments

Comments
 (0)