feat: system resources widget (#3538)

* feat: add system resources widget

* Update packages/widgets/src/system-resources/index.ts

Co-authored-by: Andre Silva <32734153+Aandree5@users.noreply.github.com>

* fix: system resources not updating

* refactor: improve logic in component

* fix: tooltip overflow

* feat: add label with last value

* feat: hide label when hovering

* fix: formatting

* fix: lint

* fix: formatting

* fix: wrong redis channel used for opnsense

---------

Co-authored-by: Andre Silva <32734153+Aandree5@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
Manuel
2025-08-04 20:12:28 +00:00
committed by GitHub
parent 1b5ccb5293
commit 3ee408bf53
24 changed files with 512 additions and 26 deletions

View File

@@ -7,7 +7,7 @@ import { z } from "zod";
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { createChannelEventHistory } from "../../../redis/src/lib/channel";
import { createChannelEventHistoryOld } from "../../../redis/src/lib/channel";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
@@ -32,14 +32,16 @@ export class DashDotIntegration extends Integration implements ISystemHealthMoni
const cpuLoad = await this.getCurrentCpuLoadAsync();
const memoryLoad = await this.getCurrentMemoryLoadAsync();
const storageLoad = await this.getCurrentStorageLoadAsync();
const networkLoad = await this.getCurrentNetworkLoadAsync();
const channel = this.getChannel();
const history = await channel.getSliceUntilTimeAsync(dayjs().subtract(15, "minutes").toDate());
return {
cpuUtilization: cpuLoad.sumLoad,
memUsed: `${memoryLoad.loadInBytes}`,
memAvailable: `${info.maxAvailableMemoryBytes - memoryLoad.loadInBytes}`,
memUsedInBytes: memoryLoad.loadInBytes,
memAvailableInBytes: info.maxAvailableMemoryBytes - memoryLoad.loadInBytes,
network: networkLoad,
fileSystem: info.storage
.filter((_, index) => storageLoad[index] !== -1) // filter out undermoutned drives, they display as -1 in the load API
.map((storage, index) => ({
@@ -113,8 +115,13 @@ export class DashDotIntegration extends Integration implements ISystemHealthMoni
};
}
private async getCurrentNetworkLoadAsync() {
const response = await fetchWithTrustedCertificatesAsync(this.url("/load/network"));
return await networkLoadApi.parseAsync(await response.json());
}
private getChannel() {
return createChannelEventHistory<z.infer<typeof cpuLoadPerCoreApiList>>(
return createChannelEventHistoryOld<z.infer<typeof cpuLoadPerCoreApiList>>(
`integration:${this.integration.id}:history:cpu`,
100,
);
@@ -130,6 +137,11 @@ const memoryLoadApi = z.object({
load: z.number().min(0),
});
const networkLoadApi = z.object({
up: z.number().min(0),
down: z.number().min(0),
});
const internalServerInfoApi = z.object({
os: z.object({
distro: z.string(),

View File

@@ -4,9 +4,13 @@ export interface SystemHealthMonitoring {
version: string;
cpuModelName: string;
cpuUtilization: number;
memUsed: string;
memAvailable: string;
memUsedInBytes: number;
memAvailableInBytes: number;
uptime: number;
network: {
up: number;
down: number;
} | null;
loadAverage: {
"1min": number;
"5min": number;

View File

@@ -7,9 +7,13 @@ export class SystemHealthMonitoringMockService implements ISystemHealthMonitorin
version: "1.0.0",
cpuModelName: "Mock CPU",
cpuUtilization: Math.random(),
memUsed: (4 * 1024 * 1024 * 1024).toString(), // 4 GB in bytes
memAvailable: (8 * 1024 * 1024 * 1024).toString(), // 8 GB in bytes
memUsedInBytes: 4 * 1024 * 1024 * 1024, // 4 GB in bytes
memAvailableInBytes: 8 * 1024 * 1024 * 1024, // 8 GB in bytes
availablePkgUpdates: 0,
network: {
up: 1024 * 16,
down: 1024 * 16 * 6,
},
rebootRequired: false,
cpuTemp: Math.floor(Math.random() * 100), // Random temperature between 0 and 99
uptime: Math.floor(Math.random() * 1000000), // Random uptime in seconds

View File

@@ -69,9 +69,11 @@ export class OpenMediaVaultIntegration extends Integration implements ISystemHea
version: systemResult.data.response.version,
cpuModelName: systemResult.data.response.cpuModelName ?? "Unknown CPU",
cpuUtilization: systemResult.data.response.cpuUtilization,
memUsed: systemResult.data.response.memUsed,
memAvailable: systemResult.data.response.memAvailable,
memUsedInBytes: Number(systemResult.data.response.memUsed),
memAvailableInBytes: Number(systemResult.data.response.memAvailable),
uptime: systemResult.data.response.uptime,
/* real-time traffic monitoring is not available over the RPC API from OMV */
network: null,
loadAverage: {
"1min": systemResult.data.response.loadAverage["1min"],
"5min": systemResult.data.response.loadAverage["5min"],

View File

@@ -1,7 +1,7 @@
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { ParseError, ResponseError } from "@homarr/common/server";
import { createChannelEventHistory } from "@homarr/redis";
import { createChannelEventHistoryOld } from "../../../redis/src/lib/channel";
import { HandleIntegrationErrors } from "../base/errors/decorator";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
@@ -64,7 +64,7 @@ export class OPNsenseIntegration extends Integration implements FirewallSummaryI
}
private getInterfacesChannel() {
return createChannelEventHistory<FirewallInterface[]>(`integration:${this.integration.id}:interfaces`, 15);
return createChannelEventHistoryOld<FirewallInterface[]>(`integration:${this.integration.id}:interfaces`, 15);
}
public async getFirewallInterfacesAsync(): Promise<FirewallInterfacesSummary[]> {