improve bandwidth measurement precision + refactor default area chart

This commit is contained in:
henrygd
2025-07-23 15:52:02 -04:00
parent 261f7fb76c
commit 46fdc94cb8
7 changed files with 210 additions and 154 deletions

View File

@@ -174,24 +174,27 @@ func (a *Agent) getSystemStats() system.Stats {
a.initializeNetIoStats() a.initializeNetIoStats()
} }
if netIO, err := psutilNet.IOCounters(true); err == nil { if netIO, err := psutilNet.IOCounters(true); err == nil {
secondsElapsed := time.Since(a.netIoStats.Time).Seconds() msElapsed := uint64(time.Since(a.netIoStats.Time).Milliseconds())
a.netIoStats.Time = time.Now() a.netIoStats.Time = time.Now()
bytesSent := uint64(0) totalBytesSent := uint64(0)
bytesRecv := uint64(0) totalBytesRecv := uint64(0)
// sum all bytes sent and received // sum all bytes sent and received
for _, v := range netIO { for _, v := range netIO {
// skip if not in valid network interfaces list // skip if not in valid network interfaces list
if _, exists := a.netInterfaces[v.Name]; !exists { if _, exists := a.netInterfaces[v.Name]; !exists {
continue continue
} }
bytesSent += v.BytesSent totalBytesSent += v.BytesSent
bytesRecv += v.BytesRecv totalBytesRecv += v.BytesRecv
} }
// add to systemStats // add to systemStats
sentPerSecond := float64(bytesSent-a.netIoStats.BytesSent) / secondsElapsed var bytesSentPerSecond, bytesRecvPerSecond uint64
recvPerSecond := float64(bytesRecv-a.netIoStats.BytesRecv) / secondsElapsed if msElapsed > 0 {
networkSentPs := bytesToMegabytes(sentPerSecond) bytesSentPerSecond = (totalBytesSent - a.netIoStats.BytesSent) * 1000 / msElapsed
networkRecvPs := bytesToMegabytes(recvPerSecond) bytesRecvPerSecond = (totalBytesRecv - a.netIoStats.BytesRecv) * 1000 / msElapsed
}
networkSentPs := bytesToMegabytes(float64(bytesSentPerSecond))
networkRecvPs := bytesToMegabytes(float64(bytesRecvPerSecond))
// add check for issue (#150) where sent is a massive number // add check for issue (#150) where sent is a massive number
if networkSentPs > 10_000 || networkRecvPs > 10_000 { if networkSentPs > 10_000 || networkRecvPs > 10_000 {
slog.Warn("Invalid net stats. Resetting.", "sent", networkSentPs, "recv", networkRecvPs) slog.Warn("Invalid net stats. Resetting.", "sent", networkSentPs, "recv", networkRecvPs)
@@ -206,9 +209,10 @@ func (a *Agent) getSystemStats() system.Stats {
} else { } else {
systemStats.NetworkSent = networkSentPs systemStats.NetworkSent = networkSentPs
systemStats.NetworkRecv = networkRecvPs systemStats.NetworkRecv = networkRecvPs
systemStats.Bandwidth[0], systemStats.Bandwidth[1] = bytesSentPerSecond, bytesRecvPerSecond
// update netIoStats // update netIoStats
a.netIoStats.BytesSent = bytesSent a.netIoStats.BytesSent = totalBytesSent
a.netIoStats.BytesRecv = bytesRecv a.netIoStats.BytesRecv = totalBytesRecv
} }
} }
@@ -257,7 +261,9 @@ func (a *Agent) getSystemStats() system.Stats {
a.systemInfo.MemPct = systemStats.MemPct a.systemInfo.MemPct = systemStats.MemPct
a.systemInfo.DiskPct = systemStats.DiskPct a.systemInfo.DiskPct = systemStats.DiskPct
a.systemInfo.Uptime, _ = host.Uptime() a.systemInfo.Uptime, _ = host.Uptime()
// TODO: in future release, remove MB bandwidth values in favor of bytes
a.systemInfo.Bandwidth = twoDecimals(systemStats.NetworkSent + systemStats.NetworkRecv) a.systemInfo.Bandwidth = twoDecimals(systemStats.NetworkSent + systemStats.NetworkRecv)
a.systemInfo.BandwidthBytes = systemStats.Bandwidth[0] + systemStats.Bandwidth[1]
slog.Debug("sysinfo", "data", a.systemInfo) slog.Debug("sysinfo", "data", a.systemInfo)
return systemStats return systemStats

View File

@@ -34,6 +34,8 @@ type Stats struct {
LoadAvg1 float64 `json:"l1,omitempty" cbor:"23,keyasint,omitempty,omitzero"` LoadAvg1 float64 `json:"l1,omitempty" cbor:"23,keyasint,omitempty,omitzero"`
LoadAvg5 float64 `json:"l5,omitempty" cbor:"24,keyasint,omitempty,omitzero"` LoadAvg5 float64 `json:"l5,omitempty" cbor:"24,keyasint,omitempty,omitzero"`
LoadAvg15 float64 `json:"l15,omitempty" cbor:"25,keyasint,omitempty,omitzero"` LoadAvg15 float64 `json:"l15,omitempty" cbor:"25,keyasint,omitempty,omitzero"`
Bandwidth [2]uint64 `json:"b,omitzero" cbor:"26,keyasint,omitzero"` // [sent bytes, recv bytes]
MaxBandwidth [2]uint64 `json:"bm,omitzero" cbor:"27,keyasint,omitzero"` // [sent bytes, recv bytes]
} }
type GPUData struct { type GPUData struct {
@@ -77,24 +79,25 @@ const (
) )
type Info struct { type Info struct {
Hostname string `json:"h" cbor:"0,keyasint"` Hostname string `json:"h" cbor:"0,keyasint"`
KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"` KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"`
Cores int `json:"c" cbor:"2,keyasint"` Cores int `json:"c" cbor:"2,keyasint"`
Threads int `json:"t,omitempty" cbor:"3,keyasint,omitempty"` Threads int `json:"t,omitempty" cbor:"3,keyasint,omitempty"`
CpuModel string `json:"m" cbor:"4,keyasint"` CpuModel string `json:"m" cbor:"4,keyasint"`
Uptime uint64 `json:"u" cbor:"5,keyasint"` Uptime uint64 `json:"u" cbor:"5,keyasint"`
Cpu float64 `json:"cpu" cbor:"6,keyasint"` Cpu float64 `json:"cpu" cbor:"6,keyasint"`
MemPct float64 `json:"mp" cbor:"7,keyasint"` MemPct float64 `json:"mp" cbor:"7,keyasint"`
DiskPct float64 `json:"dp" cbor:"8,keyasint"` DiskPct float64 `json:"dp" cbor:"8,keyasint"`
Bandwidth float64 `json:"b" cbor:"9,keyasint"` Bandwidth float64 `json:"b" cbor:"9,keyasint"`
AgentVersion string `json:"v" cbor:"10,keyasint"` AgentVersion string `json:"v" cbor:"10,keyasint"`
Podman bool `json:"p,omitempty" cbor:"11,keyasint,omitempty"` Podman bool `json:"p,omitempty" cbor:"11,keyasint,omitempty"`
GpuPct float64 `json:"g,omitempty" cbor:"12,keyasint,omitempty"` GpuPct float64 `json:"g,omitempty" cbor:"12,keyasint,omitempty"`
DashboardTemp float64 `json:"dt,omitempty" cbor:"13,keyasint,omitempty"` DashboardTemp float64 `json:"dt,omitempty" cbor:"13,keyasint,omitempty"`
Os Os `json:"os" cbor:"14,keyasint"` Os Os `json:"os" cbor:"14,keyasint"`
LoadAvg1 float64 `json:"l1,omitempty" cbor:"15,keyasint,omitempty"` LoadAvg1 float64 `json:"l1,omitempty" cbor:"15,keyasint,omitempty"`
LoadAvg5 float64 `json:"l5,omitempty" cbor:"16,keyasint,omitempty"` LoadAvg5 float64 `json:"l5,omitempty" cbor:"16,keyasint,omitempty"`
LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"` LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"`
BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"`
} }
// Final data structure to return to the hub // Final data structure to return to the hub

View File

@@ -206,12 +206,16 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
sum.LoadAvg1 += stats.LoadAvg1 sum.LoadAvg1 += stats.LoadAvg1
sum.LoadAvg5 += stats.LoadAvg5 sum.LoadAvg5 += stats.LoadAvg5
sum.LoadAvg15 += stats.LoadAvg15 sum.LoadAvg15 += stats.LoadAvg15
sum.Bandwidth[0] += stats.Bandwidth[0]
sum.Bandwidth[1] += stats.Bandwidth[1]
// Set peak values // Set peak values
sum.MaxCpu = max(sum.MaxCpu, stats.MaxCpu, stats.Cpu) sum.MaxCpu = max(sum.MaxCpu, stats.MaxCpu, stats.Cpu)
sum.MaxNetworkSent = max(sum.MaxNetworkSent, stats.MaxNetworkSent, stats.NetworkSent) sum.MaxNetworkSent = max(sum.MaxNetworkSent, stats.MaxNetworkSent, stats.NetworkSent)
sum.MaxNetworkRecv = max(sum.MaxNetworkRecv, stats.MaxNetworkRecv, stats.NetworkRecv) sum.MaxNetworkRecv = max(sum.MaxNetworkRecv, stats.MaxNetworkRecv, stats.NetworkRecv)
sum.MaxDiskReadPs = max(sum.MaxDiskReadPs, stats.MaxDiskReadPs, stats.DiskReadPs) sum.MaxDiskReadPs = max(sum.MaxDiskReadPs, stats.MaxDiskReadPs, stats.DiskReadPs)
sum.MaxDiskWritePs = max(sum.MaxDiskWritePs, stats.MaxDiskWritePs, stats.DiskWritePs) sum.MaxDiskWritePs = max(sum.MaxDiskWritePs, stats.MaxDiskWritePs, stats.DiskWritePs)
sum.MaxBandwidth[0] = max(sum.MaxBandwidth[0], stats.MaxBandwidth[0], stats.Bandwidth[0])
sum.MaxBandwidth[1] = max(sum.MaxBandwidth[1], stats.MaxBandwidth[1], stats.Bandwidth[1])
// Accumulate temperatures // Accumulate temperatures
if stats.Temperatures != nil { if stats.Temperatures != nil {
@@ -284,6 +288,8 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
sum.LoadAvg1 = twoDecimals(sum.LoadAvg1 / count) sum.LoadAvg1 = twoDecimals(sum.LoadAvg1 / count)
sum.LoadAvg5 = twoDecimals(sum.LoadAvg5 / count) sum.LoadAvg5 = twoDecimals(sum.LoadAvg5 / count)
sum.LoadAvg15 = twoDecimals(sum.LoadAvg15 / count) sum.LoadAvg15 = twoDecimals(sum.LoadAvg15 / count)
sum.Bandwidth[0] = sum.Bandwidth[0] / uint64(count)
sum.Bandwidth[1] = sum.Bandwidth[1] / uint64(count)
// Average temperatures // Average temperatures
if sum.Temperatures != nil && tempCount > 0 { if sum.Temperatures != nil && tempCount > 0 {
for key := range sum.Temperatures { for key := range sum.Temperatures {

View File

@@ -1,128 +1,91 @@
import { t } from "@lingui/core/macro"
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts" import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart" import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
import { useYAxisWidth, cn, formatShortDate, chartMargin } from "@/lib/utils" import { useYAxisWidth, cn, formatShortDate, chartMargin } from "@/lib/utils"
// import Spinner from '../spinner' import { ChartData, SystemStatsRecord } from "@/types"
import { ChartData } from "@/types" import { useMemo } from "react"
import { memo, useMemo } from "react"
import { useLingui } from "@lingui/react/macro"
/** [label, key, color, opacity] */ export type DataPoint = {
type DataKeys = [string, string, number, number] label: string
dataKey: (data: SystemStatsRecord) => number | undefined
const getNestedValue = (path: string, max = false, data: any): number | null => { color: string
// fallback value (obj?.stats?.cpum ? 0 : null) should only come into play when viewing opacity: number
// a max value which doesn't exist, or the value was zero and omitted from the stats object.
// so we check if cpum is present. if so, return 0 to make sure the zero value is displayed.
// if not, return null - there is no max data so do not display anything.
return `stats.${path}${max ? "m" : ""}`
.split(".")
.reduce((acc: any, key: string) => acc?.[key] ?? (data.stats?.cpum ? 0 : null), data)
} }
export default memo(function AreaChartDefault({ export default function AreaChartDefault({
maxToggled = false,
chartName,
chartData, chartData,
max, max,
maxToggled,
tickFormatter, tickFormatter,
contentFormatter, contentFormatter,
}: { dataPoints,
maxToggled?: boolean }: // logRender = false,
chartName: string {
chartData: ChartData chartData: ChartData
max?: number max?: number
tickFormatter: (value: number) => string maxToggled?: boolean
contentFormatter: ({ value }: { value: number }) => string tickFormatter: (value: number, index: number) => string
contentFormatter: ({ value, payload }: { value: number; payload: SystemStatsRecord }) => string
dataPoints?: DataPoint[]
// logRender?: boolean
}) { }) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
const { i18n } = useLingui()
const { chartTime } = chartData return useMemo(() => {
if (chartData.systemStats.length === 0) {
const showMax = chartTime !== "1h" && maxToggled return null
const dataKeys: DataKeys[] = useMemo(() => {
// [label, key, color, opacity]
if (chartName === "CPU Usage") {
return [[t`CPU Usage`, "cpu", 1, 0.4]]
} else if (chartName === "dio") {
return [
[t({ message: "Write", comment: "Disk write" }), "dw", 3, 0.3],
[t({ message: "Read", comment: "Disk read" }), "dr", 1, 0.3],
]
} else if (chartName === "bw") {
return [
[t({ message: "Sent", comment: "Network bytes sent (upload)" }), "ns", 5, 0.2],
[t({ message: "Received", comment: "Network bytes received (download)" }), "nr", 2, 0.2],
]
} else if (chartName.startsWith("efs")) {
return [
[t`Write`, `${chartName}.w`, 3, 0.3],
[t`Read`, `${chartName}.r`, 1, 0.3],
]
} else if (chartName.startsWith("g.")) {
return [chartName.includes("mu") ? [t`Used`, chartName, 2, 0.25] : [t`Usage`, chartName, 1, 0.4]]
} }
return [] // if (logRender) {
}, [chartName, i18n.locale]) // console.log("Rendered at", new Date())
// }
// console.log('Rendered at', new Date()) return (
<div>
if (chartData.systemStats.length === 0) { <ChartContainer
return null className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
} "opacity-100": yAxisWidth,
return (
<div>
<ChartContainer
className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
"opacity-100": yAxisWidth,
})}
>
<AreaChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}>
<CartesianGrid vertical={false} />
<YAxis
direction="ltr"
orientation={chartData.orientation}
className="tracking-tighter"
width={yAxisWidth}
domain={[0, max ?? "auto"]}
tickFormatter={(value) => updateYAxisWidth(tickFormatter(value))}
tickLine={false}
axisLine={false}
/>
{xAxis(chartData)}
<ChartTooltip
animationEasing="ease-out"
animationDuration={150}
content={
<ChartTooltipContent
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
contentFormatter={contentFormatter}
// indicator="line"
/>
}
/>
{dataKeys.map((key, i) => {
const color = `hsl(var(--chart-${key[2]}))`
return (
<Area
key={i}
dataKey={getNestedValue.bind(null, key[1], showMax)}
name={key[0]}
type="monotoneX"
fill={color}
fillOpacity={key[3]}
stroke={color}
isAnimationActive={false}
/>
)
})} })}
{/* <ChartLegend content={<ChartLegendContent />} /> */} >
</AreaChart> <AreaChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}>
</ChartContainer> <CartesianGrid vertical={false} />
</div> <YAxis
) direction="ltr"
}) orientation={chartData.orientation}
className="tracking-tighter"
width={yAxisWidth}
domain={[0, max ?? "auto"]}
tickFormatter={(value, index) => updateYAxisWidth(tickFormatter(value, index))}
tickLine={false}
axisLine={false}
/>
{xAxis(chartData)}
<ChartTooltip
animationEasing="ease-out"
animationDuration={150}
content={
<ChartTooltipContent
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
contentFormatter={contentFormatter}
/>
}
/>
{dataPoints?.map((dataPoint, i) => {
const color = `hsl(var(--chart-${dataPoint.color}))`
return (
<Area
key={i}
dataKey={dataPoint.dataKey}
name={dataPoint.label}
type="monotoneX"
fill={color}
fillOpacity={dataPoint.opacity}
stroke={color}
isAnimationActive={false}
/>
)
})}
{/* <ChartLegend content={<ChartLegendContent />} /> */}
</AreaChart>
</ChartContainer>
</div>
)
}, [chartData.systemStats.length, yAxisWidth, maxToggled])
}

View File

@@ -371,6 +371,7 @@ export default function SystemDetail({ name }: { name: string }) {
// select field for switching between avg and max values // select field for switching between avg and max values
const maxValSelect = isLongerChart ? <SelectAvgMax max={maxValues} /> : null const maxValSelect = isLongerChart ? <SelectAvgMax max={maxValues} /> : null
const showMax = chartTime !== "1h" && maxValues
// if no data, show empty message // if no data, show empty message
const dataEmpty = !chartLoading && chartData.systemStats.length === 0 const dataEmpty = !chartLoading && chartData.systemStats.length === 0
@@ -477,8 +478,15 @@ export default function SystemDetail({ name }: { name: string }) {
> >
<AreaChartDefault <AreaChartDefault
chartData={chartData} chartData={chartData}
chartName="CPU Usage"
maxToggled={maxValues} maxToggled={maxValues}
dataPoints={[
{
label: t`CPU Usage`,
dataKey: ({ stats }) => (showMax ? stats?.cpum : stats?.cpu),
color: "1",
opacity: 0.4,
},
]}
tickFormatter={(val) => toFixedFloat(val, 2) + "%"} tickFormatter={(val) => toFixedFloat(val, 2) + "%"}
contentFormatter={({ value }) => decimalString(value) + "%"} contentFormatter={({ value }) => decimalString(value) + "%"}
/> />
@@ -530,8 +538,21 @@ export default function SystemDetail({ name }: { name: string }) {
> >
<AreaChartDefault <AreaChartDefault
chartData={chartData} chartData={chartData}
chartName="dio"
maxToggled={maxValues} maxToggled={maxValues}
dataPoints={[
{
label: t({ message: "Write", comment: "Disk write" }),
dataKey: ({ stats }) => (showMax ? stats?.dwm : stats?.dw),
color: "3",
opacity: 0.3,
},
{
label: t({ message: "Read", comment: "Disk read" }),
dataKey: ({ stats }) => (showMax ? stats?.drm : stats?.dr),
color: "1",
opacity: 0.3,
},
]}
tickFormatter={(val) => { tickFormatter={(val) => {
const { value, unit } = formatBytes(val, true, userSettings.unitDisk, true) const { value, unit } = formatBytes(val, true, userSettings.unitDisk, true)
return toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit return toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit
@@ -552,15 +573,39 @@ export default function SystemDetail({ name }: { name: string }) {
> >
<AreaChartDefault <AreaChartDefault
chartData={chartData} chartData={chartData}
chartName="bw"
maxToggled={maxValues} maxToggled={maxValues}
dataPoints={[
{
label: t`Sent`,
// use bytes if available, otherwise multiply old MB (can remove in future)
dataKey(data) {
if (showMax) {
return data?.stats?.bm?.[0] ?? (data?.stats?.nsm ?? 0) * 1024 * 1024
}
return data?.stats?.b?.[0] ?? data?.stats?.ns * 1024 * 1024
},
color: "5",
opacity: 0.2,
},
{
label: t`Received`,
dataKey(data) {
if (showMax) {
return data?.stats?.bm?.[1] ?? (data?.stats?.nrm ?? 0) * 1024 * 1024
}
return data?.stats?.b?.[1] ?? data?.stats?.nr * 1024 * 1024
},
color: "2",
opacity: 0.2,
},
]}
tickFormatter={(val) => { tickFormatter={(val) => {
let { value, unit } = formatBytes(val, true, userSettings.unitNet, true) let { value, unit } = formatBytes(val, true, userSettings.unitNet, false)
return toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit return toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit
}} }}
contentFormatter={({ value }) => { contentFormatter={(data) => {
const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitNet, true) const { value, unit } = formatBytes(data.value, true, userSettings.unitNet, false)
return decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + " " + unit return decimalString(value, value >= 100 ? 1 : 2) + " " + unit
}} }}
/> />
</ChartCard> </ChartCard>
@@ -649,7 +694,14 @@ export default function SystemDetail({ name }: { name: string }) {
> >
<AreaChartDefault <AreaChartDefault
chartData={chartData} chartData={chartData}
chartName={`g.${id}.u`} dataPoints={[
{
label: t`Usage`,
dataKey: ({ stats }) => stats?.g?.[id]?.u ?? 0,
color: "1",
opacity: 0.35,
},
]}
tickFormatter={(val) => toFixedFloat(val, 2) + "%"} tickFormatter={(val) => toFixedFloat(val, 2) + "%"}
contentFormatter={({ value }) => decimalString(value) + "%"} contentFormatter={({ value }) => decimalString(value) + "%"}
/> />
@@ -662,7 +714,14 @@ export default function SystemDetail({ name }: { name: string }) {
> >
<AreaChartDefault <AreaChartDefault
chartData={chartData} chartData={chartData}
chartName={`g.${id}.mu`} dataPoints={[
{
label: t`Usage`,
dataKey: ({ stats }) => stats?.g?.[id]?.mu ?? 0,
color: "2",
opacity: 0.25,
},
]}
max={gpu.mt} max={gpu.mt}
tickFormatter={(val) => { tickFormatter={(val) => {
const { value, unit } = formatBytes(val, false, Unit.Bytes, true) const { value, unit } = formatBytes(val, false, Unit.Bytes, true)
@@ -707,7 +766,20 @@ export default function SystemDetail({ name }: { name: string }) {
> >
<AreaChartDefault <AreaChartDefault
chartData={chartData} chartData={chartData}
chartName={`efs.${extraFsName}`} dataPoints={[
{
label: t`Write`,
dataKey: ({ stats }) => stats?.efs?.[extraFsName]?.[showMax ? "wm" : "w"] ?? 0,
color: "3",
opacity: 0.3,
},
{
label: t`Read`,
dataKey: ({ stats }) => stats?.efs?.[extraFsName]?.[showMax ? "rm" : "r"] ?? 0,
color: "1",
opacity: 0.3,
},
]}
maxToggled={maxValues} maxToggled={maxValues}
tickFormatter={(val) => { tickFormatter={(val) => {
const { value, unit } = formatBytes(val, true, userSettings.unitDisk, true) const { value, unit } = formatBytes(val, true, userSettings.unitDisk, true)

View File

@@ -260,19 +260,19 @@ export default function SystemsTable() {
}, },
}, },
{ {
accessorFn: (originalRow) => originalRow.info.b || 0, accessorFn: ({ info }) => info.bb || (info.b || 0) * 1024 * 1024,
id: "net", id: "net",
name: () => t`Net`, name: () => t`Net`,
size: 0, size: 0,
Icon: EthernetIcon, Icon: EthernetIcon,
header: sortableHeader, header: sortableHeader,
cell(info) { cell(info) {
if (info.row.original.status === "paused") { const sys = info.row.original
if (sys.status === "paused") {
return null return null
} }
const val = info.getValue() as number
const userSettings = useStore($userSettings) const userSettings = useStore($userSettings)
const { value, unit } = formatBytes(val, true, userSettings.unitNet, true) const { value, unit } = formatBytes(info.getValue() as number, true, userSettings.unitNet, false)
return ( return (
<span className="tabular-nums whitespace-nowrap"> <span className="tabular-nums whitespace-nowrap">
{decimalString(value, value >= 100 ? 1 : 2)} {unit} {decimalString(value, value >= 100 ? 1 : 2)} {unit}

View File

@@ -60,6 +60,8 @@ export interface SystemInfo {
dp: number dp: number
/** bandwidth (mb) */ /** bandwidth (mb) */
b: number b: number
/** bandwidth bytes */
bb?: number
/** agent version */ /** agent version */
v: string v: string
/** system is using podman */ /** system is using podman */
@@ -115,10 +117,14 @@ export interface SystemStats {
ns: number ns: number
/** network received (mb) */ /** network received (mb) */
nr: number nr: number
/** bandwidth bytes [sent, recv] */
b?: [number, number]
/** max network sent (mb) */ /** max network sent (mb) */
nsm?: number nsm?: number
/** max network received (mb) */ /** max network received (mb) */
nrm?: number nrm?: number
/** max network sent (bytes) */
bm?: [number, number]
/** temperatures */ /** temperatures */
t?: Record<string, number> t?: Record<string, number>
/** extra filesystems */ /** extra filesystems */