Files
homarr/packages/widgets/src/system-resources/component.tsx
T
Daniel 003dac4744 feat: add GPU monitoring support via Glances integration (#5255)
* feat: add GPU monitoring support via Glances integration

- Add GPU fields to SystemHealthMonitoring interface (gpuId, name,
  memoryUtilization, processorUtilization, temperature, fanSpeed)
- Extend Glances integration to parse GPU data from /api/4/all endpoint
  (supports NVIDIA, AMD, and Intel GPUs via Glances)
- Add GPU utilization ring to Health Monitoring widget with tooltip
  showing VRAM usage, temperature, and fan speed
- Add GPU time-series chart to System Resources widget (grape color)
- Add GPU toggle option to both widgets
- Add English translations for GPU labels
- Add gpu: [] to all other integrations (dashdot, truenas, unraid,
  openmediavault) for backward compatibility
- Add GPU mock data for development/testing

Closes community request from PR #5102 for GPU monitoring support.

* fix: implement GPU monitoring for dashdot integration and clarify OMV comment

- Add GPU load fetching via dashdot /load/gpu endpoint
- Add GPU info parsing from dashdot /info endpoint
- Add gpuLoadApi and gpu field to internalServerInfoApi schemas
- Add getCurrentGpuLoadAsync method to DashDotIntegration
- Map GPU load data to SystemHealthMonitoring gpu array
- Add explanatory comment for OMV's empty gpu array

Addresses review comments from manuel-rw on PR #5255

* Delete manifest.json
2026-03-27 20:22:41 +01:00

130 lines
4.5 KiB
TypeScript

"use client";
import { useState } from "react";
import { Box, Group, Stack } from "@mantine/core";
import { useElementSize } from "@mantine/hooks";
import { clientApi } from "@homarr/api/client";
import type { WidgetComponentProps } from "../definition";
import { CombinedNetworkTrafficChart } from "./chart/combined-network-traffic";
import { SystemResourceCPUChart } from "./chart/cpu-chart";
import { SystemResourceGPUChart } from "./chart/gpu-chart";
import { SystemResourceMemoryChart } from "./chart/memory-chart";
import { NetworkTrafficChart } from "./chart/network-traffic";
const MAX_QUEUE_SIZE = 15;
export default function SystemResources({ integrationIds, options }: WidgetComponentProps<"systemResources">) {
const { ref, width } = useElementSize();
const [data] = clientApi.widget.healthMonitoring.getSystemHealthStatus.useSuspenseQuery({
integrationIds,
});
const memoryCapacityInBytes =
(data[0]?.healthInfo.memAvailableInBytes ?? 0) + (data[0]?.healthInfo.memUsedInBytes ?? 0);
const [items, setItems] = useState<
{ cpu: number; memory: number; gpu: number; network: { up: number; down: number } | null }[]
>(
data.map((item) => ({
cpu: item.healthInfo.cpuUtilization,
memory: item.healthInfo.memUsedInBytes,
gpu:
item.healthInfo.gpu.length > 0
? item.healthInfo.gpu.reduce((acc, g) => acc + g.processorUtilization, 0) / item.healthInfo.gpu.length
: 0,
network: item.healthInfo.network,
})),
);
clientApi.widget.healthMonitoring.subscribeSystemHealthStatus.useSubscription(
{
integrationIds,
},
{
onData(data) {
setItems((previousItems) => {
const next = {
cpu: data.healthInfo.cpuUtilization,
memory: data.healthInfo.memUsedInBytes,
gpu:
data.healthInfo.gpu.length > 0
? data.healthInfo.gpu.reduce((acc, g) => acc + g.processorUtilization, 0) / data.healthInfo.gpu.length
: 0,
network: data.healthInfo.network,
};
return [...previousItems, next].slice(-MAX_QUEUE_SIZE);
});
},
},
);
const showNetwork =
items.length === 0 || (items.every((item) => item.network !== null) && options.visibleCharts.includes("network"));
const rowHeight = `calc((100% - ${(options.visibleCharts.length - 1) * 8}px) / ${options.visibleCharts.length})`;
return (
<Stack gap="xs" p="xs" ref={ref} h="100%">
{options.visibleCharts.includes("cpu") && (
<Box h={rowHeight}>
<SystemResourceCPUChart
cpuUsageOverTime={items.map((item) => item.cpu)}
hasShadow={options.hasShadow}
labelDisplayMode={options.labelDisplayMode}
/>
</Box>
)}
{options.visibleCharts.includes("memory") && (
<Box h={rowHeight}>
<SystemResourceMemoryChart
memoryUsageOverTime={items.map((item) => item.memory)}
totalCapacityInBytes={memoryCapacityInBytes}
hasShadow={options.hasShadow}
labelDisplayMode={options.labelDisplayMode}
/>
</Box>
)}
{options.visibleCharts.includes("gpu") && (
<Box h={rowHeight}>
<SystemResourceGPUChart
gpuUsageOverTime={items.map((item) => item.gpu)}
hasShadow={options.hasShadow}
labelDisplayMode={options.labelDisplayMode}
/>
</Box>
)}
{showNetwork &&
(width > 256 ? (
<Group h={rowHeight} gap="xs" grow>
<NetworkTrafficChart
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
usageOverTime={items.map((item) => item.network!.down)}
isUp={false}
hasShadow={options.hasShadow}
labelDisplayMode={options.labelDisplayMode}
/>
<NetworkTrafficChart
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
usageOverTime={items.map((item) => item.network!.up)}
isUp
hasShadow={options.hasShadow}
labelDisplayMode={options.labelDisplayMode}
/>
</Group>
) : (
<Box h={rowHeight}>
<CombinedNetworkTrafficChart
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
usageOverTime={items.map((item) => item.network!)}
hasShadow={options.hasShadow}
labelDisplayMode={options.labelDisplayMode}
/>
</Box>
))}
</Stack>
);
}