[Feature] Expand system info bar to include memory, disk, CPU, and OS details (#952)

* collect OS info

* Fix systeminfo

* Fix it

* optimize it

* Add disk info

* add ethernet info

* add ethernet

* remove speed from ethernet

* add cpu info

* chore cleanup data

* chore fix podman

* restruct systeminfo

* use short cpu name

* debug memory

* collect and show memory

* remove os from the table

* truncate nic name

* chore: shorter names in json

* collect memory info

* add debug

* undo memory

* revert package.json

* fix conflicts

* fix conflixts

* Fix MacOs os family

* add ISP data for remote systems

* reorder the system page bar information

* remove OS from the system table

* Update with main

* Fix vulcheck

* Fix systembar

* fix system bar

* fix vulcheck

* update struct with static info

* Adjust collection method to upon agent connection
This commit is contained in:
Sven van Ginkel
2025-12-13 22:11:31 +01:00
committed by GitHub
parent 35329abcbd
commit d71714cbba
11 changed files with 504 additions and 62 deletions

View File

@@ -115,6 +115,37 @@ const (
Freebsd
)
type DiskInfo struct {
Name string `json:"n"`
Model string `json:"m,omitempty"`
Vendor string `json:"v,omitempty"`
}
type NetworkInfo struct {
Name string `json:"n"`
Vendor string `json:"v,omitempty"`
Model string `json:"m,omitempty"`
Speed string `json:"s,omitempty"`
}
type MemoryInfo struct {
Total string `json:"t,omitempty"`
}
type CpuInfo struct {
Model string `json:"m"`
SpeedGHz string `json:"s"`
Arch string `json:"a"`
Cores int `json:"c"`
Threads int `json:"t"`
}
type OsInfo struct {
Family string `json:"f"`
Version string `json:"v"`
Kernel string `json:"k"`
}
type ConnectionType = uint8
const (
@@ -123,26 +154,35 @@ const (
ConnectionTypeWebSocket
)
// StaticInfo contains system information that rarely or never changes
// This is collected at a longer interval (e.g., 10-15 minutes) to reduce bandwidth
type StaticInfo struct {
Hostname string `json:"h" cbor:"0,keyasint"`
KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"`
Threads int `json:"t,omitempty" cbor:"2,keyasint,omitempty"`
AgentVersion string `json:"v" cbor:"3,keyasint"`
Podman bool `json:"p,omitempty" cbor:"4,keyasint,omitempty"`
Os Os `json:"os" cbor:"5,keyasint"`
Disks []DiskInfo `json:"d,omitempty" cbor:"6,omitempty"`
Networks []NetworkInfo `json:"n,omitempty" cbor:"7,omitempty"`
Memory []MemoryInfo `json:"m" cbor:"8"`
Cpus []CpuInfo `json:"c" cbor:"9"`
Oses []OsInfo `json:"o,omitempty" cbor:"10,omitempty"`
}
// Info contains frequently-changing system snapshot data for the dashboard
type Info struct {
Hostname string `json:"h" cbor:"0,keyasint"`
KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"`
Cores int `json:"c" cbor:"2,keyasint"`
Threads int `json:"t,omitempty" cbor:"3,keyasint,omitempty"`
CpuModel string `json:"m" cbor:"4,keyasint"`
Uptime uint64 `json:"u" cbor:"5,keyasint"`
Cpu float64 `json:"cpu" cbor:"6,keyasint"`
MemPct float64 `json:"mp" cbor:"7,keyasint"`
DiskPct float64 `json:"dp" cbor:"8,keyasint"`
Bandwidth float64 `json:"b" cbor:"9,keyasint"`
AgentVersion string `json:"v" cbor:"10,keyasint"`
Podman bool `json:"p,omitempty" cbor:"11,keyasint,omitempty"`
GpuPct float64 `json:"g,omitempty" cbor:"12,keyasint,omitempty"`
DashboardTemp float64 `json:"dt,omitempty" cbor:"13,keyasint,omitempty"`
Os Os `json:"os" cbor:"14,keyasint"`
LoadAvg1 float64 `json:"l1,omitempty" cbor:"15,keyasint,omitempty"`
LoadAvg5 float64 `json:"l5,omitempty" cbor:"16,keyasint,omitempty"`
LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"`
BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"`
Uptime uint64 `json:"u" cbor:"0,keyasint"`
Cpu float64 `json:"cpu" cbor:"1,keyasint"`
MemPct float64 `json:"mp" cbor:"2,keyasint"`
DiskPct float64 `json:"dp" cbor:"3,keyasint"`
Bandwidth float64 `json:"b" cbor:"4,keyasint"`
GpuPct float64 `json:"g,omitempty" cbor:"5,keyasint,omitempty"`
DashboardTemp float64 `json:"dt,omitempty" cbor:"6,keyasint,omitempty"`
LoadAvg1 float64 `json:"l1,omitempty" cbor:"7,keyasint,omitempty"`
LoadAvg5 float64 `json:"l5,omitempty" cbor:"8,keyasint,omitempty"`
LoadAvg15 float64 `json:"l15,omitempty" cbor:"9,keyasint,omitempty"`
BandwidthBytes uint64 `json:"bb" cbor:"10,keyasint"`
// TODO: remove load fields in future release in favor of load avg array
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"`
ConnectionType ConnectionType `json:"ct,omitempty" cbor:"20,keyasint,omitempty,omitzero"`
@@ -157,4 +197,5 @@ type CombinedData struct {
Info Info `json:"info" cbor:"1,keyasint"`
Containers []*container.Stats `json:"container" cbor:"2,keyasint"`
SystemdServices []*systemd.Service `json:"systemd,omitempty" cbor:"3,keyasint,omitempty"`
StaticInfo *StaticInfo `json:"static_info,omitempty" cbor:"4,keyasint,omitempty"` // Collected at longer intervals
}

View File

@@ -36,6 +36,7 @@ type System struct {
manager *SystemManager // Manager that this system belongs to
client *ssh.Client // SSH client for fetching data
data *system.CombinedData // system data from agent
staticInfo *system.StaticInfo // cached static system info, fetched once per connection
ctx context.Context // Context for stopping the updater
cancel context.CancelFunc // Stops and removes system from updater
WsConn *ws.WsConn // Handler for agent WebSocket connection
@@ -114,8 +115,22 @@ func (sys *System) update() error {
sys.handlePaused()
return nil
}
data, err := sys.fetchDataFromAgent(common.DataRequestOptions{CacheTimeMs: uint16(interval)})
// Determine which cache time to use based on whether we need static info
cacheTimeMs := uint16(interval)
if sys.staticInfo == nil {
// Request with a cache time that signals the agent to include static info
// We use 60001ms (just above the standard interval) since uint16 max is 65535
cacheTimeMs = 60_001
}
data, err := sys.fetchDataFromAgent(common.DataRequestOptions{CacheTimeMs: cacheTimeMs})
if err == nil {
// If we received static info, cache it
if data.StaticInfo != nil {
sys.staticInfo = data.StaticInfo
sys.manager.hub.Logger().Debug("Cached static system info", "system", sys.Id)
}
_, err = sys.createRecords(data)
}
return err
@@ -136,6 +151,11 @@ func (sys *System) handlePaused() {
// createRecords updates the system record and adds system_stats and container_stats records
func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error) {
// Build complete info combining dynamic and static data
completeInfo := sys.buildCompleteInfo(data)
sys.manager.hub.Logger().Debug("Creating records - complete info", "info", completeInfo)
systemRecord, err := sys.getRecord()
if err != nil {
return nil, err
@@ -186,7 +206,7 @@ func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error
// update system record (do this last because it triggers alerts and we need above records to be inserted first)
systemRecord.Set("status", up)
systemRecord.Set("info", data.Info)
systemRecord.Set("info", completeInfo)
if err := txApp.SaveNoValidate(systemRecord); err != nil {
return err
}
@@ -203,6 +223,70 @@ func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error
return systemRecord, err
}
// buildCompleteInfo combines the dynamic Info with cached StaticInfo to create a complete system info structure
// This is needed because we've split the original Info structure for bandwidth optimization
func (sys *System) buildCompleteInfo(data *system.CombinedData) map[string]interface{} {
info := make(map[string]interface{})
// Add dynamic fields from data.Info
if data.Info.Uptime > 0 {
info["u"] = data.Info.Uptime
}
info["cpu"] = data.Info.Cpu
info["mp"] = data.Info.MemPct
info["dp"] = data.Info.DiskPct
info["b"] = data.Info.Bandwidth
info["bb"] = data.Info.BandwidthBytes
if data.Info.GpuPct > 0 {
info["g"] = data.Info.GpuPct
}
if data.Info.DashboardTemp > 0 {
info["dt"] = data.Info.DashboardTemp
}
if data.Info.LoadAvg1 > 0 || data.Info.LoadAvg5 > 0 || data.Info.LoadAvg15 > 0 {
info["l1"] = data.Info.LoadAvg1
info["l5"] = data.Info.LoadAvg5
info["l15"] = data.Info.LoadAvg15
info["la"] = data.Info.LoadAvg
}
if data.Info.ConnectionType > 0 {
info["ct"] = data.Info.ConnectionType
}
// Add static fields from cached staticInfo
if sys.staticInfo != nil {
info["h"] = sys.staticInfo.Hostname
if sys.staticInfo.KernelVersion != "" {
info["k"] = sys.staticInfo.KernelVersion
}
if sys.staticInfo.Threads > 0 {
info["t"] = sys.staticInfo.Threads
}
info["v"] = sys.staticInfo.AgentVersion
if sys.staticInfo.Podman {
info["p"] = true
}
info["os"] = sys.staticInfo.Os
if len(sys.staticInfo.Cpus) > 0 {
info["c"] = sys.staticInfo.Cpus
}
if len(sys.staticInfo.Memory) > 0 {
info["m"] = sys.staticInfo.Memory
}
if len(sys.staticInfo.Disks) > 0 {
info["d"] = sys.staticInfo.Disks
}
if len(sys.staticInfo.Networks) > 0 {
info["n"] = sys.staticInfo.Networks
}
if len(sys.staticInfo.Oses) > 0 {
info["o"] = sys.staticInfo.Oses
}
}
return info
}
func createSystemdStatsRecords(app core.App, data []*systemd.Service, systemId string) error {
if len(data) == 0 {
return nil
@@ -602,6 +686,7 @@ func (sys *System) closeSSHConnection() {
sys.client.Close()
sys.client = nil
}
sys.staticInfo = nil
}
// closeWebSocketConnection closes the WebSocket connection but keeps the system in the manager
@@ -611,6 +696,7 @@ func (sys *System) closeWebSocketConnection() {
if sys.WsConn != nil {
sys.WsConn.Close(nil)
}
sys.staticInfo = nil
}
// extractAgentVersion extracts the beszel version from SSH server version string

View File

@@ -8,8 +8,10 @@ import {
ClockArrowUp,
CpuIcon,
GlobeIcon,
HardDriveIcon,
LayoutGridIcon,
MonitorIcon,
ServerIcon,
XIcon,
} from "lucide-react"
import { subscribeKeys } from "nanostores"
@@ -66,7 +68,7 @@ import { $router, navigate } from "../router"
import Spinner from "../spinner"
import { Button } from "../ui/button"
import { Card, CardDescription, CardHeader, CardTitle } from "../ui/card"
import { AppleIcon, ChartAverage, ChartMax, FreeBsdIcon, Rows, TuxIcon, WebSocketIcon, WindowsIcon } from "../ui/icons"
import { AppleIcon, ChartAverage, ChartMax, EthernetIcon, FreeBsdIcon, Rows, TuxIcon, WebSocketIcon, WindowsIcon } from "../ui/icons"
import { Input } from "../ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
import { Separator } from "../ui/separator"
@@ -333,6 +335,20 @@ export default memo(function SystemDetail({ id }: { id: string }) {
})
}, [system, chartTime])
// Helper to format hardware info (disk/nic) with vendor and model
const formatHardwareInfo = useCallback((item: { n: string; v?: string; m?: string }) => {
const vendor = item.v && item.v.toLowerCase() !== 'unknown' ? item.v : null
const model = item.m && item.m.toLowerCase() !== 'unknown' ? item.m : null
if (vendor && model) {
return `${item.n}: ${vendor} ${model}`
} else if (model) {
return `${item.n}: ${model}`
} else if (vendor) {
return `${item.n}: ${vendor}`
}
return item.n
}, [])
// values for system info bar
const systemInfo = useMemo(() => {
if (!system.info) {
@@ -366,6 +382,11 @@ export default memo(function SystemDetail({ id }: { id: string }) {
} else {
uptime = secondsToString(system.info.u, "day")
}
// Extract CPU and Memory info from arrays
const cpuInfo = system.info.c && system.info.c.length > 0 ? system.info.c[0] : undefined
const memoryInfo = system.info.m && system.info.m.length > 0 ? system.info.m[0] : undefined
const osData = system.info.o && system.info.o.length > 0 ? system.info.o[0] : undefined
return [
{ value: getHostDisplayValue(system), Icon: GlobeIcon },
{
@@ -376,19 +397,43 @@ export default memo(function SystemDetail({ id }: { id: string }) {
hide: system.info.h === system.host || system.info.h === system.name,
},
{ value: uptime, Icon: ClockArrowUp, label: t`Uptime`, hide: !system.info.u },
osInfo[system.info.os ?? Os.Linux],
{
value: `${system.info.m} (${system.info.c}c${system.info.t ? `/${system.info.t}t` : ""})`,
osData ? {
value: `${osData.f} ${osData.v}`.trim(),
Icon: osInfo[system.info.os ?? Os.Linux]?.Icon ?? TuxIcon,
label: osData.k ? `Kernel: ${osData.k}` : undefined,
} : osInfo[system.info.os ?? Os.Linux],
cpuInfo ? {
value: cpuInfo.m,
Icon: CpuIcon,
hide: !system.info.m,
},
] as {
hide: !cpuInfo.m,
label: [
(cpuInfo.c || cpuInfo.t) ? `Cores / Threads: ${cpuInfo.c || '?'} / ${cpuInfo.t || cpuInfo.c || '?'}` : null,
cpuInfo.a ? `Arch: ${cpuInfo.a}` : null,
cpuInfo.s ? `Speed: ${cpuInfo.s}` : null,
].filter(Boolean).join('\n'),
} : undefined,
memoryInfo ? {
value: memoryInfo.t,
Icon: ServerIcon,
label: "Total Memory",
} : undefined,
system.info.d && system.info.d.length > 0 ? {
value: `${system.info.d.length} ${system.info.d.length === 1 ? t`Disk` : t`Disks`}`,
Icon: HardDriveIcon,
label: system.info.d.map(formatHardwareInfo).join('\n'),
} : undefined,
system.info.n && system.info.n.length > 0 ? {
value: `${system.info.n.length} ${system.info.n.length === 1 ? t`NIC` : t`NICs`}`,
Icon: EthernetIcon,
label: system.info.n.map(formatHardwareInfo).join('\n'),
} : undefined,
].filter(Boolean) as {
value: string | number | undefined
label?: string
Icon: React.ElementType
hide?: boolean
}[]
}, [system, t])
}, [system, t, formatHardwareInfo])
/** Space for tooltip if more than 10 sensors and no containers table */
useEffect(() => {

View File

@@ -27,12 +27,32 @@ export interface SystemRecord extends RecordModel {
host: string
status: "up" | "down" | "paused" | "pending"
port: string
info: SystemInfo
info: systemInfo
v: string
updated: string
}
export interface SystemInfo {
export interface CpuInfo {
m: string
s: string
a: string
c: number
t: number
}
export interface OsInfo {
f: string
v: string
k: string
}
export interface NetworkLocationInfo {
ip?: string
isp?: string
asn?: string
}
export interface systemInfo {
/** hostname */
h: string
/** kernel **/
@@ -75,6 +95,16 @@ export interface SystemInfo {
g?: number
/** dashboard display temperature */
dt?: number
/** disks info (array of block devices with model/vendor/serial) */
d?: { n: string; m?: string; v?: string; serial?: string }[]
/** networks info (array of network interfaces with vendor/model/capabilities) */
n?: { n: string; v?: string; m?: string; s?: string }[]
/** memory info (array with total property) */
m?: { t: string }[]
/** cpu info (array of cpu objects) */
c?: CpuInfo[]
/** os info (array of os objects) */
o?: OsInfo[]
/** operating system */
os?: Os
/** connection type */