mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-22 05:36:15 +01:00
Compare commits
8 Commits
adbfe7cfb7
...
split-syst
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
330d375997 | ||
|
|
8627e3ee97 | ||
|
|
5d04ee5a65 | ||
|
|
d93067ec34 | ||
|
|
82bd953941 | ||
|
|
996444abeb | ||
|
|
aef4baff5e | ||
|
|
3dea061e93 |
@@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/gliderlabs/ssh"
|
"github.com/gliderlabs/ssh"
|
||||||
"github.com/henrygd/beszel"
|
"github.com/henrygd/beszel"
|
||||||
"github.com/henrygd/beszel/agent/deltatracker"
|
"github.com/henrygd/beszel/agent/deltatracker"
|
||||||
|
"github.com/henrygd/beszel/internal/common"
|
||||||
"github.com/henrygd/beszel/internal/entities/system"
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
"github.com/shirou/gopsutil/v4/host"
|
"github.com/shirou/gopsutil/v4/host"
|
||||||
gossh "golang.org/x/crypto/ssh"
|
gossh "golang.org/x/crypto/ssh"
|
||||||
@@ -37,7 +38,8 @@ type Agent struct {
|
|||||||
netInterfaceDeltaTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64] // Per-cache-time NIC delta trackers
|
netInterfaceDeltaTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64] // Per-cache-time NIC delta trackers
|
||||||
dockerManager *dockerManager // Manages Docker API requests
|
dockerManager *dockerManager // Manages Docker API requests
|
||||||
sensorConfig *SensorConfig // Sensors config
|
sensorConfig *SensorConfig // Sensors config
|
||||||
systemInfo system.Info // Host system info
|
systemInfo system.Info // Host system info (dynamic)
|
||||||
|
systemDetails system.Details // Host system details (static, once-per-connection)
|
||||||
gpuManager *GPUManager // Manages GPU data
|
gpuManager *GPUManager // Manages GPU data
|
||||||
cache *systemDataCache // Cache for system stats based on cache time
|
cache *systemDataCache // Cache for system stats based on cache time
|
||||||
connectionManager *ConnectionManager // Channel to signal connection events
|
connectionManager *ConnectionManager // Channel to signal connection events
|
||||||
@@ -97,8 +99,11 @@ func NewAgent(dataDir ...string) (agent *Agent, err error) {
|
|||||||
|
|
||||||
slog.Debug(beszel.Version)
|
slog.Debug(beszel.Version)
|
||||||
|
|
||||||
|
// initialize docker manager
|
||||||
|
agent.dockerManager = newDockerManager()
|
||||||
|
|
||||||
// initialize system info
|
// initialize system info
|
||||||
agent.initializeSystemInfo()
|
agent.refreshStaticInfo()
|
||||||
|
|
||||||
// initialize connection manager
|
// initialize connection manager
|
||||||
agent.connectionManager = newConnectionManager(agent)
|
agent.connectionManager = newConnectionManager(agent)
|
||||||
@@ -112,9 +117,6 @@ func NewAgent(dataDir ...string) (agent *Agent, err error) {
|
|||||||
// initialize net io stats
|
// initialize net io stats
|
||||||
agent.initializeNetIoStats()
|
agent.initializeNetIoStats()
|
||||||
|
|
||||||
// initialize docker manager
|
|
||||||
agent.dockerManager = newDockerManager(agent)
|
|
||||||
|
|
||||||
agent.systemdManager, err = newSystemdManager()
|
agent.systemdManager, err = newSystemdManager()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Debug("Systemd", "err", err)
|
slog.Debug("Systemd", "err", err)
|
||||||
@@ -133,7 +135,7 @@ func NewAgent(dataDir ...string) (agent *Agent, err error) {
|
|||||||
|
|
||||||
// if debugging, print stats
|
// if debugging, print stats
|
||||||
if agent.debug {
|
if agent.debug {
|
||||||
slog.Debug("Stats", "data", agent.gatherStats(0))
|
slog.Debug("Stats", "data", agent.gatherStats(common.DataRequestOptions{CacheTimeMs: 60_000, IncludeDetails: true}))
|
||||||
}
|
}
|
||||||
|
|
||||||
return agent, nil
|
return agent, nil
|
||||||
@@ -148,10 +150,11 @@ func GetEnv(key string) (value string, exists bool) {
|
|||||||
return os.LookupEnv(key)
|
return os.LookupEnv(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) gatherStats(cacheTimeMs uint16) *system.CombinedData {
|
func (a *Agent) gatherStats(options common.DataRequestOptions) *system.CombinedData {
|
||||||
a.Lock()
|
a.Lock()
|
||||||
defer a.Unlock()
|
defer a.Unlock()
|
||||||
|
|
||||||
|
cacheTimeMs := options.CacheTimeMs
|
||||||
data, isCached := a.cache.Get(cacheTimeMs)
|
data, isCached := a.cache.Get(cacheTimeMs)
|
||||||
if isCached {
|
if isCached {
|
||||||
slog.Debug("Cached data", "cacheTimeMs", cacheTimeMs)
|
slog.Debug("Cached data", "cacheTimeMs", cacheTimeMs)
|
||||||
@@ -162,6 +165,12 @@ func (a *Agent) gatherStats(cacheTimeMs uint16) *system.CombinedData {
|
|||||||
Stats: a.getSystemStats(cacheTimeMs),
|
Stats: a.getSystemStats(cacheTimeMs),
|
||||||
Info: a.systemInfo,
|
Info: a.systemInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Include static info only when requested
|
||||||
|
if options.IncludeDetails {
|
||||||
|
data.Details = &a.systemDetails
|
||||||
|
}
|
||||||
|
|
||||||
// slog.Info("System data", "data", data, "cacheTimeMs", cacheTimeMs)
|
// slog.Info("System data", "data", data, "cacheTimeMs", cacheTimeMs)
|
||||||
|
|
||||||
if a.dockerManager != nil {
|
if a.dockerManager != nil {
|
||||||
@@ -225,7 +234,7 @@ func (a *Agent) getFingerprint() string {
|
|||||||
// if no fingerprint is found, generate one
|
// if no fingerprint is found, generate one
|
||||||
fingerprint, err := host.HostID()
|
fingerprint, err := host.HostID()
|
||||||
if err != nil || fingerprint == "" {
|
if err != nil || fingerprint == "" {
|
||||||
fingerprint = a.systemInfo.Hostname + a.systemInfo.CpuModel
|
fingerprint = a.systemDetails.Hostname + a.systemDetails.CpuModel
|
||||||
}
|
}
|
||||||
|
|
||||||
// hash fingerprint
|
// hash fingerprint
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func createTestCacheData() *system.CombinedData {
|
|||||||
DiskTotal: 100000,
|
DiskTotal: 100000,
|
||||||
},
|
},
|
||||||
Info: system.Info{
|
Info: system.Info{
|
||||||
Hostname: "test-host",
|
AgentVersion: "0.12.0",
|
||||||
},
|
},
|
||||||
Containers: []*container.Stats{
|
Containers: []*container.Stats{
|
||||||
{
|
{
|
||||||
@@ -128,7 +128,7 @@ func TestCacheMultipleIntervals(t *testing.T) {
|
|||||||
Mem: 16384,
|
Mem: 16384,
|
||||||
},
|
},
|
||||||
Info: system.Info{
|
Info: system.Info{
|
||||||
Hostname: "test-host-2",
|
AgentVersion: "0.12.0",
|
||||||
},
|
},
|
||||||
Containers: []*container.Stats{},
|
Containers: []*container.Stats{},
|
||||||
}
|
}
|
||||||
@@ -171,7 +171,7 @@ func TestCacheOverwrite(t *testing.T) {
|
|||||||
Mem: 32768,
|
Mem: 32768,
|
||||||
},
|
},
|
||||||
Info: system.Info{
|
Info: system.Info{
|
||||||
Hostname: "updated-host",
|
AgentVersion: "0.12.0",
|
||||||
},
|
},
|
||||||
Containers: []*container.Stats{},
|
Containers: []*container.Stats{},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ func (client *WebSocketClient) handleAuthChallenge(msg *common.HubRequest[cbor.R
|
|||||||
|
|
||||||
if authRequest.NeedSysInfo {
|
if authRequest.NeedSysInfo {
|
||||||
response.Name, _ = GetEnv("SYSTEM_NAME")
|
response.Name, _ = GetEnv("SYSTEM_NAME")
|
||||||
response.Hostname = client.agent.systemInfo.Hostname
|
response.Hostname = client.agent.systemDetails.Hostname
|
||||||
serverAddr := client.agent.connectionManager.serverOptions.Addr
|
serverAddr := client.agent.connectionManager.serverOptions.Addr
|
||||||
_, response.Port, _ = net.SplitHostPort(serverAddr)
|
_, response.Port, _ = net.SplitHostPort(serverAddr)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ type dockerManager struct {
|
|||||||
decoder *json.Decoder // Reusable JSON decoder that reads from buf
|
decoder *json.Decoder // Reusable JSON decoder that reads from buf
|
||||||
apiStats *container.ApiStats // Reusable API stats object
|
apiStats *container.ApiStats // Reusable API stats object
|
||||||
excludeContainers []string // Patterns to exclude containers by name
|
excludeContainers []string // Patterns to exclude containers by name
|
||||||
|
usingPodman bool // Whether the Docker Engine API is running on Podman
|
||||||
|
|
||||||
// Cache-time-aware tracking for CPU stats (similar to cpu.go)
|
// Cache-time-aware tracking for CPU stats (similar to cpu.go)
|
||||||
// Maps cache time intervals to container-specific CPU usage tracking
|
// Maps cache time intervals to container-specific CPU usage tracking
|
||||||
@@ -478,7 +479,7 @@ func (dm *dockerManager) deleteContainerStatsSync(id string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new http client for Docker or Podman API
|
// Creates a new http client for Docker or Podman API
|
||||||
func newDockerManager(a *Agent) *dockerManager {
|
func newDockerManager() *dockerManager {
|
||||||
dockerHost, exists := GetEnv("DOCKER_HOST")
|
dockerHost, exists := GetEnv("DOCKER_HOST")
|
||||||
if exists {
|
if exists {
|
||||||
// return nil if set to empty string
|
// return nil if set to empty string
|
||||||
@@ -564,7 +565,7 @@ func newDockerManager(a *Agent) *dockerManager {
|
|||||||
|
|
||||||
// If using podman, return client
|
// If using podman, return client
|
||||||
if strings.Contains(dockerHost, "podman") {
|
if strings.Contains(dockerHost, "podman") {
|
||||||
a.systemInfo.Podman = true
|
manager.usingPodman = true
|
||||||
manager.goodDockerVersion = true
|
manager.goodDockerVersion = true
|
||||||
return manager
|
return manager
|
||||||
}
|
}
|
||||||
@@ -746,3 +747,23 @@ func decodeDockerLogStream(reader io.Reader, builder *strings.Builder) error {
|
|||||||
totalBytesRead += int(n)
|
totalBytesRead += int(n)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetHostInfo fetches the system info from Docker
|
||||||
|
func (dm *dockerManager) GetHostInfo() (info container.HostInfo, err error) {
|
||||||
|
resp, err := dm.client.Get("http://localhost/info")
|
||||||
|
if err != nil {
|
||||||
|
return info, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
|
||||||
|
slog.Error("Failed to decode Docker version response", "error", err)
|
||||||
|
return info, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dm *dockerManager) IsPodman() bool {
|
||||||
|
return dm.usingPodman
|
||||||
|
}
|
||||||
|
|||||||
@@ -802,6 +802,24 @@ func TestNetworkRateCalculationFormula(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetHostInfo(t *testing.T) {
|
||||||
|
data, err := os.ReadFile("test-data/system_info.json")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var info container.HostInfo
|
||||||
|
err = json.Unmarshal(data, &info)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "6.8.0-31-generic", info.KernelVersion)
|
||||||
|
assert.Equal(t, "Ubuntu 24.04 LTS", info.OperatingSystem)
|
||||||
|
// assert.Equal(t, "24.04", info.OSVersion)
|
||||||
|
// assert.Equal(t, "linux", info.OSType)
|
||||||
|
// assert.Equal(t, "x86_64", info.Architecture)
|
||||||
|
assert.EqualValues(t, 4, info.NCPU)
|
||||||
|
assert.EqualValues(t, 2095882240, info.MemTotal)
|
||||||
|
// assert.Equal(t, "27.0.1", info.ServerVersion)
|
||||||
|
}
|
||||||
|
|
||||||
func TestDeltaTrackerCacheTimeIsolation(t *testing.T) {
|
func TestDeltaTrackerCacheTimeIsolation(t *testing.T) {
|
||||||
// Test that different cache times have separate DeltaTracker instances
|
// Test that different cache times have separate DeltaTracker instances
|
||||||
dm := &dockerManager{
|
dm := &dockerManager{
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ func (h *GetDataHandler) Handle(hctx *HandlerContext) error {
|
|||||||
var options common.DataRequestOptions
|
var options common.DataRequestOptions
|
||||||
_ = cbor.Unmarshal(hctx.Request.Data, &options)
|
_ = cbor.Unmarshal(hctx.Request.Data, &options)
|
||||||
|
|
||||||
sysStats := hctx.Agent.gatherStats(options.CacheTimeMs)
|
sysStats := hctx.Agent.gatherStats(options)
|
||||||
return hctx.SendResponse(sysStats, hctx.RequestID)
|
return hctx.SendResponse(sysStats, hctx.RequestID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ func (a *Agent) handleSSHRequest(w io.Writer, req *common.HubRequest[cbor.RawMes
|
|||||||
|
|
||||||
// handleLegacyStats serves the legacy one-shot stats payload for older hubs
|
// handleLegacyStats serves the legacy one-shot stats payload for older hubs
|
||||||
func (a *Agent) handleLegacyStats(w io.Writer, hubVersion semver.Version) error {
|
func (a *Agent) handleLegacyStats(w io.Writer, hubVersion semver.Version) error {
|
||||||
stats := a.gatherStats(60_000)
|
stats := a.gatherStats(common.DataRequestOptions{CacheTimeMs: 60_000})
|
||||||
return a.writeToSession(w, stats, hubVersion)
|
return a.writeToSession(w, stats, hubVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -513,7 +513,7 @@ func TestWriteToSessionEncoding(t *testing.T) {
|
|||||||
err = json.Unmarshal([]byte(encodedData), &decodedJson)
|
err = json.Unmarshal([]byte(encodedData), &decodedJson)
|
||||||
assert.Error(t, err, "Should not be valid JSON data")
|
assert.Error(t, err, "Should not be valid JSON data")
|
||||||
|
|
||||||
assert.Equal(t, testData.Info.Hostname, decodedCbor.Info.Hostname)
|
assert.Equal(t, testData.Details.Hostname, decodedCbor.Details.Hostname)
|
||||||
assert.Equal(t, testData.Stats.Cpu, decodedCbor.Stats.Cpu)
|
assert.Equal(t, testData.Stats.Cpu, decodedCbor.Stats.Cpu)
|
||||||
} else {
|
} else {
|
||||||
// Should be JSON - try to decode as JSON
|
// Should be JSON - try to decode as JSON
|
||||||
@@ -526,7 +526,7 @@ func TestWriteToSessionEncoding(t *testing.T) {
|
|||||||
assert.Error(t, err, "Should not be valid CBOR data")
|
assert.Error(t, err, "Should not be valid CBOR data")
|
||||||
|
|
||||||
// Verify the decoded JSON data matches our test data
|
// Verify the decoded JSON data matches our test data
|
||||||
assert.Equal(t, testData.Info.Hostname, decodedJson.Info.Hostname)
|
assert.Equal(t, testData.Details.Hostname, decodedJson.Details.Hostname)
|
||||||
assert.Equal(t, testData.Stats.Cpu, decodedJson.Stats.Cpu)
|
assert.Equal(t, testData.Stats.Cpu, decodedJson.Stats.Cpu)
|
||||||
|
|
||||||
// Verify it looks like JSON (starts with '{' and contains readable field names)
|
// Verify it looks like JSON (starts with '{' and contains readable field names)
|
||||||
@@ -551,12 +551,8 @@ func createTestCombinedData() *system.CombinedData {
|
|||||||
DiskPct: 50.0,
|
DiskPct: 50.0,
|
||||||
},
|
},
|
||||||
Info: system.Info{
|
Info: system.Info{
|
||||||
Hostname: "test-host",
|
|
||||||
Cores: 8,
|
|
||||||
CpuModel: "Test CPU Model",
|
|
||||||
Uptime: 3600,
|
Uptime: 3600,
|
||||||
AgentVersion: "0.12.0",
|
AgentVersion: "0.12.0",
|
||||||
Os: system.Linux,
|
|
||||||
},
|
},
|
||||||
Containers: []*container.Stats{
|
Containers: []*container.Stats{
|
||||||
{
|
{
|
||||||
|
|||||||
113
agent/system.go
113
agent/system.go
@@ -2,15 +2,18 @@ package agent
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/henrygd/beszel"
|
"github.com/henrygd/beszel"
|
||||||
"github.com/henrygd/beszel/agent/battery"
|
"github.com/henrygd/beszel/agent/battery"
|
||||||
|
"github.com/henrygd/beszel/internal/entities/container"
|
||||||
"github.com/henrygd/beszel/internal/entities/system"
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
|
|
||||||
"github.com/shirou/gopsutil/v4/cpu"
|
"github.com/shirou/gopsutil/v4/cpu"
|
||||||
@@ -27,41 +30,79 @@ type prevDisk struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sets initial / non-changing values about the host system
|
// Sets initial / non-changing values about the host system
|
||||||
func (a *Agent) initializeSystemInfo() {
|
func (a *Agent) refreshStaticInfo() {
|
||||||
a.systemInfo.AgentVersion = beszel.Version
|
a.systemInfo.AgentVersion = beszel.Version
|
||||||
a.systemInfo.Hostname, _ = os.Hostname()
|
|
||||||
|
// get host info from Docker if available
|
||||||
|
var hostInfo container.HostInfo
|
||||||
|
|
||||||
|
if a.dockerManager != nil {
|
||||||
|
a.systemDetails.Podman = a.dockerManager.IsPodman()
|
||||||
|
hostInfo, _ = a.dockerManager.GetHostInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
a.systemDetails.Hostname, _ = os.Hostname()
|
||||||
|
if arch, err := host.KernelArch(); err == nil {
|
||||||
|
a.systemDetails.Arch = arch
|
||||||
|
} else {
|
||||||
|
a.systemDetails.Arch = runtime.GOARCH
|
||||||
|
}
|
||||||
|
|
||||||
platform, _, version, _ := host.PlatformInformation()
|
platform, _, version, _ := host.PlatformInformation()
|
||||||
|
|
||||||
if platform == "darwin" {
|
if platform == "darwin" {
|
||||||
a.systemInfo.KernelVersion = version
|
a.systemDetails.Os = system.Darwin
|
||||||
a.systemInfo.Os = system.Darwin
|
a.systemDetails.OsName = fmt.Sprintf("macOS %s", version)
|
||||||
} else if strings.Contains(platform, "indows") {
|
} else if strings.Contains(platform, "indows") {
|
||||||
a.systemInfo.KernelVersion = fmt.Sprintf("%s %s", strings.Replace(platform, "Microsoft ", "", 1), version)
|
a.systemDetails.Os = system.Windows
|
||||||
a.systemInfo.Os = system.Windows
|
a.systemDetails.OsName = strings.Replace(platform, "Microsoft ", "", 1)
|
||||||
|
a.systemDetails.Kernel = version
|
||||||
} else if platform == "freebsd" {
|
} else if platform == "freebsd" {
|
||||||
a.systemInfo.Os = system.Freebsd
|
a.systemDetails.Os = system.Freebsd
|
||||||
a.systemInfo.KernelVersion = version
|
a.systemDetails.Kernel, _ = host.KernelVersion()
|
||||||
|
if prettyName, err := getOsPrettyName(); err == nil {
|
||||||
|
a.systemDetails.OsName = prettyName
|
||||||
|
} else {
|
||||||
|
a.systemDetails.OsName = "FreeBSD"
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
a.systemInfo.Os = system.Linux
|
a.systemDetails.Os = system.Linux
|
||||||
}
|
a.systemDetails.OsName = hostInfo.OperatingSystem
|
||||||
|
if a.systemDetails.OsName == "" {
|
||||||
if a.systemInfo.KernelVersion == "" {
|
if prettyName, err := getOsPrettyName(); err == nil {
|
||||||
a.systemInfo.KernelVersion, _ = host.KernelVersion()
|
a.systemDetails.OsName = prettyName
|
||||||
|
} else {
|
||||||
|
a.systemDetails.OsName = platform
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.systemDetails.Kernel = hostInfo.KernelVersion
|
||||||
|
if a.systemDetails.Kernel == "" {
|
||||||
|
a.systemDetails.Kernel, _ = host.KernelVersion()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// cpu model
|
// cpu model
|
||||||
if info, err := cpu.Info(); err == nil && len(info) > 0 {
|
if info, err := cpu.Info(); err == nil && len(info) > 0 {
|
||||||
a.systemInfo.CpuModel = info[0].ModelName
|
a.systemDetails.CpuModel = info[0].ModelName
|
||||||
}
|
}
|
||||||
// cores / threads
|
// cores / threads
|
||||||
a.systemInfo.Cores, _ = cpu.Counts(false)
|
cores, _ := cpu.Counts(false)
|
||||||
if threads, err := cpu.Counts(true); err == nil {
|
threads := hostInfo.NCPU
|
||||||
if threads > 0 && threads < a.systemInfo.Cores {
|
if threads == 0 {
|
||||||
// in lxc logical cores reflects container limits, so use that as cores if lower
|
threads, _ = cpu.Counts(true)
|
||||||
a.systemInfo.Cores = threads
|
}
|
||||||
} else {
|
// in lxc, logical cores reflects container limits, so use that as cores if lower
|
||||||
a.systemInfo.Threads = threads
|
if threads > 0 && threads < cores {
|
||||||
|
cores = threads
|
||||||
|
}
|
||||||
|
a.systemDetails.Cores = cores
|
||||||
|
a.systemDetails.Threads = threads
|
||||||
|
|
||||||
|
// total memory
|
||||||
|
a.systemDetails.MemoryTotal = hostInfo.MemTotal
|
||||||
|
if a.systemDetails.MemoryTotal == 0 {
|
||||||
|
if v, err := mem.VirtualMemory(); err == nil {
|
||||||
|
a.systemDetails.MemoryTotal = v.Total
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,21 +236,16 @@ func (a *Agent) getSystemStats(cacheTimeMs uint16) system.Stats {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// update base system info
|
// update system info
|
||||||
a.systemInfo.ConnectionType = a.connectionManager.ConnectionType
|
a.systemInfo.ConnectionType = a.connectionManager.ConnectionType
|
||||||
a.systemInfo.Cpu = systemStats.Cpu
|
a.systemInfo.Cpu = systemStats.Cpu
|
||||||
a.systemInfo.LoadAvg = systemStats.LoadAvg
|
a.systemInfo.LoadAvg = systemStats.LoadAvg
|
||||||
// TODO: remove these in future release in favor of load avg array
|
|
||||||
a.systemInfo.LoadAvg1 = systemStats.LoadAvg[0]
|
|
||||||
a.systemInfo.LoadAvg5 = systemStats.LoadAvg[1]
|
|
||||||
a.systemInfo.LoadAvg15 = systemStats.LoadAvg[2]
|
|
||||||
a.systemInfo.MemPct = systemStats.MemPct
|
a.systemInfo.MemPct = systemStats.MemPct
|
||||||
a.systemInfo.DiskPct = systemStats.DiskPct
|
a.systemInfo.DiskPct = systemStats.DiskPct
|
||||||
a.systemInfo.Battery = systemStats.Battery
|
a.systemInfo.Battery = systemStats.Battery
|
||||||
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.BandwidthBytes = systemStats.Bandwidth[0] + systemStats.Bandwidth[1]
|
a.systemInfo.BandwidthBytes = systemStats.Bandwidth[0] + systemStats.Bandwidth[1]
|
||||||
|
a.systemInfo.Threads = a.systemDetails.Threads
|
||||||
slog.Debug("sysinfo", "data", a.systemInfo)
|
slog.Debug("sysinfo", "data", a.systemInfo)
|
||||||
|
|
||||||
return systemStats
|
return systemStats
|
||||||
@@ -240,3 +276,24 @@ func getARCSize() (uint64, error) {
|
|||||||
|
|
||||||
return 0, fmt.Errorf("failed to parse size field")
|
return 0, fmt.Errorf("failed to parse size field")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getOsPrettyName attempts to get the pretty OS name from /etc/os-release on Linux systems
|
||||||
|
func getOsPrettyName() (string, error) {
|
||||||
|
file, err := os.Open("/etc/os-release")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if after, ok := strings.CutPrefix(line, "PRETTY_NAME="); ok {
|
||||||
|
value := after
|
||||||
|
value = strings.Trim(value, `"`)
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", errors.New("pretty name not found")
|
||||||
|
}
|
||||||
|
|||||||
17
agent/test-data/system_info.json
Normal file
17
agent/test-data/system_info.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"ID": "7TRN:IPZB:QYBB:VPBQ:UMPP:KARE:6ZNR:XE6T:7EWV:PKF4:ZOJD:TPYS",
|
||||||
|
"Containers": 14,
|
||||||
|
"ContainersRunning": 3,
|
||||||
|
"ContainersPaused": 1,
|
||||||
|
"ContainersStopped": 10,
|
||||||
|
"Images": 508,
|
||||||
|
"Driver": "overlay2",
|
||||||
|
"KernelVersion": "6.8.0-31-generic",
|
||||||
|
"OperatingSystem": "Ubuntu 24.04 LTS",
|
||||||
|
"OSVersion": "24.04",
|
||||||
|
"OSType": "linux",
|
||||||
|
"Architecture": "x86_64",
|
||||||
|
"NCPU": 4,
|
||||||
|
"MemTotal": 2095882240,
|
||||||
|
"ServerVersion": "27.0.1"
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import "github.com/blang/semver"
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// Version is the current version of the application.
|
// Version is the current version of the application.
|
||||||
Version = "0.17.0"
|
Version = "0.18.0-beta.1"
|
||||||
// AppName is the name of the application.
|
// AppName is the name of the application.
|
||||||
AppName = "beszel"
|
AppName = "beszel"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -60,10 +60,10 @@ func TestBatteryAlertLogic(t *testing.T) {
|
|||||||
combinedDataHigh := &system.CombinedData{
|
combinedDataHigh := &system.CombinedData{
|
||||||
Stats: statsHigh,
|
Stats: statsHigh,
|
||||||
Info: system.Info{
|
Info: system.Info{
|
||||||
Hostname: "test-host",
|
AgentVersion: "0.12.0",
|
||||||
Cpu: 10,
|
Cpu: 10,
|
||||||
MemPct: 30,
|
MemPct: 30,
|
||||||
DiskPct: 40,
|
DiskPct: 40,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,10 +100,10 @@ func TestBatteryAlertLogic(t *testing.T) {
|
|||||||
combinedDataLow := &system.CombinedData{
|
combinedDataLow := &system.CombinedData{
|
||||||
Stats: statsLow,
|
Stats: statsLow,
|
||||||
Info: system.Info{
|
Info: system.Info{
|
||||||
Hostname: "test-host",
|
AgentVersion: "0.12.0",
|
||||||
Cpu: 10,
|
Cpu: 10,
|
||||||
MemPct: 30,
|
MemPct: 30,
|
||||||
DiskPct: 40,
|
DiskPct: 40,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,10 +142,10 @@ func TestBatteryAlertLogic(t *testing.T) {
|
|||||||
combinedDataRecovered := &system.CombinedData{
|
combinedDataRecovered := &system.CombinedData{
|
||||||
Stats: statsRecovered,
|
Stats: statsRecovered,
|
||||||
Info: system.Info{
|
Info: system.Info{
|
||||||
Hostname: "test-host",
|
AgentVersion: "0.12.0",
|
||||||
Cpu: 10,
|
Cpu: 10,
|
||||||
MemPct: 30,
|
MemPct: 30,
|
||||||
DiskPct: 40,
|
DiskPct: 40,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,10 +198,10 @@ func TestBatteryAlertNoBattery(t *testing.T) {
|
|||||||
combinedData := &system.CombinedData{
|
combinedData := &system.CombinedData{
|
||||||
Stats: statsNoBattery,
|
Stats: statsNoBattery,
|
||||||
Info: system.Info{
|
Info: system.Info{
|
||||||
Hostname: "test-host",
|
AgentVersion: "0.12.0",
|
||||||
Cpu: 10,
|
Cpu: 10,
|
||||||
MemPct: 30,
|
MemPct: 30,
|
||||||
DiskPct: 40,
|
DiskPct: 40,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,10 +294,10 @@ func TestBatteryAlertAveragedSamples(t *testing.T) {
|
|||||||
Battery: [2]uint8{15, 1},
|
Battery: [2]uint8{15, 1},
|
||||||
},
|
},
|
||||||
Info: system.Info{
|
Info: system.Info{
|
||||||
Hostname: "test-host",
|
AgentVersion: "0.12.0",
|
||||||
Cpu: 10,
|
Cpu: 10,
|
||||||
MemPct: 30,
|
MemPct: 30,
|
||||||
DiskPct: 40,
|
DiskPct: 40,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,10 +360,10 @@ func TestBatteryAlertAveragedSamples(t *testing.T) {
|
|||||||
Battery: [2]uint8{50, 1},
|
Battery: [2]uint8{50, 1},
|
||||||
},
|
},
|
||||||
Info: system.Info{
|
Info: system.Info{
|
||||||
Hostname: "test-host",
|
AgentVersion: "0.12.0",
|
||||||
Cpu: 10,
|
Cpu: 10,
|
||||||
MemPct: 30,
|
MemPct: 30,
|
||||||
DiskPct: 40,
|
DiskPct: 40,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ type FingerprintResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DataRequestOptions struct {
|
type DataRequestOptions struct {
|
||||||
CacheTimeMs uint16 `cbor:"0,keyasint"`
|
CacheTimeMs uint16 `cbor:"0,keyasint"`
|
||||||
// ResourceType uint8 `cbor:"1,keyasint,omitempty,omitzero"`
|
IncludeDetails bool `cbor:"1,keyasint"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContainerLogsRequest struct {
|
type ContainerLogsRequest struct {
|
||||||
|
|||||||
@@ -34,6 +34,17 @@ type ApiStats struct {
|
|||||||
MemoryStats MemoryStats `json:"memory_stats"`
|
MemoryStats MemoryStats `json:"memory_stats"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Docker system info from /info
|
||||||
|
type HostInfo struct {
|
||||||
|
OperatingSystem string `json:"OperatingSystem"`
|
||||||
|
KernelVersion string `json:"KernelVersion"`
|
||||||
|
NCPU int `json:"NCPU"`
|
||||||
|
MemTotal uint64 `json:"MemTotal"`
|
||||||
|
// OSVersion string `json:"OSVersion"`
|
||||||
|
// OSType string `json:"OSType"`
|
||||||
|
// Architecture string `json:"Architecture"`
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ApiStats) CalculateCpuPercentLinux(prevCpuContainer uint64, prevCpuSystem uint64) float64 {
|
func (s *ApiStats) CalculateCpuPercentLinux(prevCpuContainer uint64, prevCpuSystem uint64) float64 {
|
||||||
cpuDelta := s.CPUStats.CPUUsage.TotalUsage - prevCpuContainer
|
cpuDelta := s.CPUStats.CPUUsage.TotalUsage - prevCpuContainer
|
||||||
systemDelta := s.CPUStats.SystemUsage - prevCpuSystem
|
systemDelta := s.CPUStats.SystemUsage - prevCpuSystem
|
||||||
|
|||||||
@@ -123,27 +123,29 @@ const (
|
|||||||
ConnectionTypeWebSocket
|
ConnectionTypeWebSocket
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Core system data that is needed in All Systems table
|
||||||
type Info struct {
|
type Info struct {
|
||||||
Hostname string `json:"h" cbor:"0,keyasint"`
|
Hostname string `json:"h,omitempty" cbor:"0,keyasint,omitempty"` // deprecated - moved to Details struct
|
||||||
KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"`
|
KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"` // deprecated - moved to Details struct
|
||||||
Cores int `json:"c" cbor:"2,keyasint"`
|
Cores int `json:"c,omitzero" cbor:"2,keyasint,omitzero"` // deprecated - moved to Details struct
|
||||||
|
CpuModel string `json:"m,omitempty" cbor:"4,keyasint,omitempty"` // deprecated - moved to Details struct
|
||||||
|
Podman bool `json:"p,omitempty" cbor:"11,keyasint,omitempty"` // deprecated - moved to Details struct
|
||||||
|
Os Os `json:"os,omitempty" cbor:"14,keyasint,omitempty"` // deprecated - moved to Details struct
|
||||||
|
// Threads is needed in Info struct to calculate load average thresholds
|
||||||
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"`
|
|
||||||
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"`
|
|
||||||
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"`
|
LoadAvg1 float64 `json:"l1,omitempty" cbor:"15,keyasint,omitempty"` // deprecated - use `la` array instead
|
||||||
LoadAvg1 float64 `json:"l1,omitempty" cbor:"15,keyasint,omitempty"`
|
LoadAvg5 float64 `json:"l5,omitempty" cbor:"16,keyasint,omitempty"` // deprecated - use `la` array instead
|
||||||
LoadAvg5 float64 `json:"l5,omitempty" cbor:"16,keyasint,omitempty"`
|
LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"` // deprecated - use `la` array instead
|
||||||
LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"`
|
|
||||||
BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"`
|
BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"`
|
||||||
// TODO: remove load fields in future release in favor of load avg array
|
|
||||||
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"`
|
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"`
|
||||||
ConnectionType ConnectionType `json:"ct,omitempty" cbor:"20,keyasint,omitempty,omitzero"`
|
ConnectionType ConnectionType `json:"ct,omitempty" cbor:"20,keyasint,omitempty,omitzero"`
|
||||||
ExtraFsPct map[string]float64 `json:"efs,omitempty" cbor:"21,keyasint,omitempty"`
|
ExtraFsPct map[string]float64 `json:"efs,omitempty" cbor:"21,keyasint,omitempty"`
|
||||||
@@ -151,10 +153,25 @@ type Info struct {
|
|||||||
Battery [2]uint8 `json:"bat,omitzero" cbor:"23,keyasint,omitzero"` // [percent, charge state]
|
Battery [2]uint8 `json:"bat,omitzero" cbor:"23,keyasint,omitzero"` // [percent, charge state]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Data that does not change during process lifetime and is not needed in All Systems table
|
||||||
|
type Details struct {
|
||||||
|
Hostname string `cbor:"0,keyasint"`
|
||||||
|
Kernel string `cbor:"1,keyasint,omitempty"`
|
||||||
|
Cores int `cbor:"2,keyasint"`
|
||||||
|
Threads int `cbor:"3,keyasint"`
|
||||||
|
CpuModel string `cbor:"4,keyasint"`
|
||||||
|
Os Os `cbor:"5,keyasint"`
|
||||||
|
OsName string `cbor:"6,keyasint"`
|
||||||
|
Arch string `cbor:"7,keyasint"`
|
||||||
|
Podman bool `cbor:"8,keyasint,omitempty"`
|
||||||
|
MemoryTotal uint64 `cbor:"9,keyasint"`
|
||||||
|
}
|
||||||
|
|
||||||
// Final data structure to return to the hub
|
// Final data structure to return to the hub
|
||||||
type CombinedData struct {
|
type CombinedData struct {
|
||||||
Stats Stats `json:"stats" cbor:"0,keyasint"`
|
Stats Stats `json:"stats" cbor:"0,keyasint"`
|
||||||
Info Info `json:"info" cbor:"1,keyasint"`
|
Info Info `json:"info" cbor:"1,keyasint"`
|
||||||
Containers []*container.Stats `json:"container" cbor:"2,keyasint"`
|
Containers []*container.Stats `json:"container" cbor:"2,keyasint"`
|
||||||
SystemdServices []*systemd.Service `json:"systemd,omitempty" cbor:"3,keyasint,omitempty"`
|
SystemdServices []*systemd.Service `json:"systemd,omitempty" cbor:"3,keyasint,omitempty"`
|
||||||
|
Details *Details `cbor:"4,keyasint,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/internal/common"
|
"github.com/henrygd/beszel/internal/common"
|
||||||
@@ -29,19 +29,21 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type System struct {
|
type System struct {
|
||||||
Id string `db:"id"`
|
Id string `db:"id"`
|
||||||
Host string `db:"host"`
|
Host string `db:"host"`
|
||||||
Port string `db:"port"`
|
Port string `db:"port"`
|
||||||
Status string `db:"status"`
|
Status string `db:"status"`
|
||||||
manager *SystemManager // Manager that this system belongs to
|
manager *SystemManager // Manager that this system belongs to
|
||||||
client *ssh.Client // SSH client for fetching data
|
client *ssh.Client // SSH client for fetching data
|
||||||
data *system.CombinedData // system data from agent
|
data *system.CombinedData // system data from agent
|
||||||
ctx context.Context // Context for stopping the updater
|
ctx context.Context // Context for stopping the updater
|
||||||
cancel context.CancelFunc // Stops and removes system from updater
|
cancel context.CancelFunc // Stops and removes system from updater
|
||||||
WsConn *ws.WsConn // Handler for agent WebSocket connection
|
WsConn *ws.WsConn // Handler for agent WebSocket connection
|
||||||
agentVersion semver.Version // Agent version
|
agentVersion semver.Version // Agent version
|
||||||
updateTicker *time.Ticker // Ticker for updating the system
|
updateTicker *time.Ticker // Ticker for updating the system
|
||||||
smartOnce sync.Once // Once for fetching and saving smart devices
|
detailsFetched atomic.Bool // True if static system details have been fetched and saved
|
||||||
|
smartFetched atomic.Bool // True if SMART devices have been fetched and saved
|
||||||
|
smartFetching atomic.Bool // True if SMART devices are currently being fetched
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *SystemManager) NewSystem(systemId string) *System {
|
func (sm *SystemManager) NewSystem(systemId string) *System {
|
||||||
@@ -114,7 +116,14 @@ func (sys *System) update() error {
|
|||||||
sys.handlePaused()
|
sys.handlePaused()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
data, err := sys.fetchDataFromAgent(common.DataRequestOptions{CacheTimeMs: uint16(interval)})
|
options := common.DataRequestOptions{
|
||||||
|
CacheTimeMs: uint16(interval),
|
||||||
|
}
|
||||||
|
// fetch system details if not already fetched
|
||||||
|
if !sys.detailsFetched.Load() {
|
||||||
|
options.IncludeDetails = true
|
||||||
|
}
|
||||||
|
data, err := sys.fetchDataFromAgent(options)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
_, err = sys.createRecords(data)
|
_, err = sys.createRecords(data)
|
||||||
}
|
}
|
||||||
@@ -142,12 +151,11 @@ func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error
|
|||||||
}
|
}
|
||||||
hub := sys.manager.hub
|
hub := sys.manager.hub
|
||||||
err = hub.RunInTransaction(func(txApp core.App) error {
|
err = hub.RunInTransaction(func(txApp core.App) error {
|
||||||
// add system_stats and container_stats records
|
// add system_stats record
|
||||||
systemStatsCollection, err := txApp.FindCachedCollectionByNameOrId("system_stats")
|
systemStatsCollection, err := txApp.FindCachedCollectionByNameOrId("system_stats")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
systemStatsRecord := core.NewRecord(systemStatsCollection)
|
systemStatsRecord := core.NewRecord(systemStatsCollection)
|
||||||
systemStatsRecord.Set("system", systemRecord.Id)
|
systemStatsRecord.Set("system", systemRecord.Id)
|
||||||
systemStatsRecord.Set("stats", data.Stats)
|
systemStatsRecord.Set("stats", data.Stats)
|
||||||
@@ -155,14 +163,14 @@ func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error
|
|||||||
if err := txApp.SaveNoValidate(systemStatsRecord); err != nil {
|
if err := txApp.SaveNoValidate(systemStatsRecord); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add containers and container_stats records
|
||||||
if len(data.Containers) > 0 {
|
if len(data.Containers) > 0 {
|
||||||
// add / update containers records
|
|
||||||
if data.Containers[0].Id != "" {
|
if data.Containers[0].Id != "" {
|
||||||
if err := createContainerRecords(txApp, data.Containers, sys.Id); err != nil {
|
if err := createContainerRecords(txApp, data.Containers, sys.Id); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// add new container_stats record
|
|
||||||
containerStatsCollection, err := txApp.FindCachedCollectionByNameOrId("container_stats")
|
containerStatsCollection, err := txApp.FindCachedCollectionByNameOrId("container_stats")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -183,9 +191,16 @@ func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add system details record
|
||||||
|
if data.Details != nil {
|
||||||
|
if err := createSystemDetailsRecord(txApp, data.Details, sys.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sys.detailsFetched.Store(true)
|
||||||
|
}
|
||||||
|
|
||||||
// update system record (do this last because it triggers alerts and we need above records to be inserted first)
|
// 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("status", up)
|
||||||
|
|
||||||
systemRecord.Set("info", data.Info)
|
systemRecord.Set("info", data.Info)
|
||||||
if err := txApp.SaveNoValidate(systemRecord); err != nil {
|
if err := txApp.SaveNoValidate(systemRecord); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -195,14 +210,44 @@ func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error
|
|||||||
|
|
||||||
// Fetch and save SMART devices when system first comes online
|
// Fetch and save SMART devices when system first comes online
|
||||||
if err == nil {
|
if err == nil {
|
||||||
sys.smartOnce.Do(func() {
|
if !sys.smartFetched.Load() && sys.smartFetching.CompareAndSwap(false, true) {
|
||||||
go sys.FetchAndSaveSmartDevices()
|
go func() {
|
||||||
})
|
defer sys.smartFetching.Store(false)
|
||||||
|
if err := sys.FetchAndSaveSmartDevices(); err == nil {
|
||||||
|
sys.smartFetched.Store(true)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return systemRecord, err
|
return systemRecord, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createSystemDetailsRecord(app core.App, data *system.Details, systemId string) error {
|
||||||
|
collectionName := "system_details"
|
||||||
|
params := dbx.Params{
|
||||||
|
"id": systemId,
|
||||||
|
"system": systemId,
|
||||||
|
"hostname": data.Hostname,
|
||||||
|
"kernel": data.Kernel,
|
||||||
|
"cores": data.Cores,
|
||||||
|
"threads": data.Threads,
|
||||||
|
"cpu": data.CpuModel,
|
||||||
|
"os": data.Os,
|
||||||
|
"os_name": data.OsName,
|
||||||
|
"arch": data.Arch,
|
||||||
|
"memory": data.MemoryTotal,
|
||||||
|
"podman": data.Podman,
|
||||||
|
"updated": time.Now().UTC(),
|
||||||
|
}
|
||||||
|
result, err := app.DB().Update(collectionName, params, dbx.HashExp{"id": systemId}).Execute()
|
||||||
|
rowsAffected, _ := result.RowsAffected()
|
||||||
|
if err != nil || rowsAffected == 0 {
|
||||||
|
_, err = app.DB().Insert(collectionName, params).Execute()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func createSystemdStatsRecords(app core.App, data []*systemd.Service, systemId string) error {
|
func createSystemdStatsRecords(app core.App, data []*systemd.Service, systemId string) error {
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -350,7 +395,8 @@ func (sys *System) fetchStringFromAgentViaSSH(action common.WebSocketAction, req
|
|||||||
if err := session.Shell(); err != nil {
|
if err := session.Shell(); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
req := common.HubRequest[any]{Action: action, Data: requestData}
|
reqDataBytes, _ := cbor.Marshal(requestData)
|
||||||
|
req := common.HubRequest[cbor.RawMessage]{Action: action, Data: reqDataBytes}
|
||||||
_ = cbor.NewEncoder(stdin).Encode(req)
|
_ = cbor.NewEncoder(stdin).Encode(req)
|
||||||
_ = stdin.Close()
|
_ = stdin.Close()
|
||||||
var resp common.AgentResponse
|
var resp common.AgentResponse
|
||||||
@@ -414,7 +460,8 @@ func (sys *System) FetchSystemdInfoFromAgent(serviceName string) (systemd.Servic
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
req := common.HubRequest[any]{Action: common.GetSystemdInfo, Data: common.SystemdInfoRequest{ServiceName: serviceName}}
|
reqDataBytes, _ := cbor.Marshal(common.SystemdInfoRequest{ServiceName: serviceName})
|
||||||
|
req := common.HubRequest[cbor.RawMessage]{Action: common.GetSystemdInfo, Data: reqDataBytes}
|
||||||
if err := cbor.NewEncoder(stdin).Encode(req); err != nil {
|
if err := cbor.NewEncoder(stdin).Encode(req); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -462,7 +509,8 @@ func (sys *System) fetchDataViaSSH(options common.DataRequestOptions) (*system.C
|
|||||||
*sys.data = system.CombinedData{}
|
*sys.data = system.CombinedData{}
|
||||||
|
|
||||||
if sys.agentVersion.GTE(beszel.MinVersionAgentResponse) && stdinErr == nil {
|
if sys.agentVersion.GTE(beszel.MinVersionAgentResponse) && stdinErr == nil {
|
||||||
req := common.HubRequest[any]{Action: common.GetData, Data: options}
|
reqDataBytes, _ := cbor.Marshal(options)
|
||||||
|
req := common.HubRequest[cbor.RawMessage]{Action: common.GetData, Data: reqDataBytes}
|
||||||
_ = cbor.NewEncoder(stdin).Encode(req)
|
_ = cbor.NewEncoder(stdin).Encode(req)
|
||||||
_ = stdin.Close()
|
_ = stdin.Close()
|
||||||
|
|
||||||
|
|||||||
@@ -266,18 +266,20 @@ func testOld(t *testing.T, hub *tests.TestHub) {
|
|||||||
|
|
||||||
// Create test system data
|
// Create test system data
|
||||||
testData := &system.CombinedData{
|
testData := &system.CombinedData{
|
||||||
|
Details: &system.Details{
|
||||||
|
Hostname: "data-test.example.com",
|
||||||
|
Kernel: "5.15.0-generic",
|
||||||
|
Cores: 4,
|
||||||
|
Threads: 8,
|
||||||
|
CpuModel: "Test CPU",
|
||||||
|
},
|
||||||
Info: system.Info{
|
Info: system.Info{
|
||||||
Hostname: "data-test.example.com",
|
Uptime: 3600,
|
||||||
KernelVersion: "5.15.0-generic",
|
Cpu: 25.5,
|
||||||
Cores: 4,
|
MemPct: 40.2,
|
||||||
Threads: 8,
|
DiskPct: 60.0,
|
||||||
CpuModel: "Test CPU",
|
Bandwidth: 100.0,
|
||||||
Uptime: 3600,
|
AgentVersion: "1.0.0",
|
||||||
Cpu: 25.5,
|
|
||||||
MemPct: 40.2,
|
|
||||||
DiskPct: 60.0,
|
|
||||||
Bandwidth: 100.0,
|
|
||||||
AgentVersion: "1.0.0",
|
|
||||||
},
|
},
|
||||||
Stats: system.Stats{
|
Stats: system.Stats{
|
||||||
Cpu: 25.5,
|
Cpu: 25.5,
|
||||||
|
|||||||
@@ -1439,6 +1439,184 @@ func init() {
|
|||||||
"type": "base",
|
"type": "base",
|
||||||
"updateRule": null,
|
"updateRule": null,
|
||||||
"viewRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id"
|
"viewRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"createRule": "",
|
||||||
|
"deleteRule": "",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "[a-z0-9]{15}",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text3208210256",
|
||||||
|
"max": 15,
|
||||||
|
"min": 15,
|
||||||
|
"name": "id",
|
||||||
|
"pattern": "^[a-z0-9]+$",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": true,
|
||||||
|
"required": true,
|
||||||
|
"system": true,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cascadeDelete": true,
|
||||||
|
"collectionId": "2hz5ncl8tizk5nx",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "relation3377271179",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"minSelect": 0,
|
||||||
|
"name": "system",
|
||||||
|
"presentable": false,
|
||||||
|
"required": true,
|
||||||
|
"system": false,
|
||||||
|
"type": "relation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text3847340049",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "hostname",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number1789936913",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "os",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text2818598173",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "os_name",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text1574083243",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "kernel",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text3128971310",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "cpu",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text4161937994",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "arch",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number4245036687",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "cores",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number1871592925",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "threads",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number3933025333",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "memory",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "bool2200265312",
|
||||||
|
"name": "podman",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "autodate3332085495",
|
||||||
|
"name": "updated",
|
||||||
|
"onCreate": true,
|
||||||
|
"onUpdate": true,
|
||||||
|
"presentable": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "autodate"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": "pbc_3116237454",
|
||||||
|
"indexes": [],
|
||||||
|
"listRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id",
|
||||||
|
"name": "system_details",
|
||||||
|
"system": false,
|
||||||
|
"type": "base",
|
||||||
|
"updateRule": "",
|
||||||
|
"viewRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id"
|
||||||
}
|
}
|
||||||
]`
|
]`
|
||||||
|
|
||||||
@@ -33,10 +33,7 @@
|
|||||||
"noUnusedFunctionParameters": "error",
|
"noUnusedFunctionParameters": "error",
|
||||||
"noUnusedPrivateClassMembers": "error",
|
"noUnusedPrivateClassMembers": "error",
|
||||||
"useExhaustiveDependencies": {
|
"useExhaustiveDependencies": {
|
||||||
"level": "warn",
|
"level": "off"
|
||||||
"options": {
|
|
||||||
"reportUnnecessaryDependencies": false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"useUniqueElementIds": "off",
|
"useUniqueElementIds": "off",
|
||||||
"noUnusedVariables": "error"
|
"noUnusedVariables": "error"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "beszel",
|
"name": "beszel",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.17.0",
|
"version": "0.18.0-beta.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host",
|
"dev": "vite --host",
|
||||||
|
|||||||
@@ -3,15 +3,7 @@ import { Trans, useLingui } from "@lingui/react/macro"
|
|||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
import { getPagePath } from "@nanostores/router"
|
import { getPagePath } from "@nanostores/router"
|
||||||
import { timeTicks } from "d3-time"
|
import { timeTicks } from "d3-time"
|
||||||
import {
|
import { XIcon } from "lucide-react"
|
||||||
ChevronRightSquareIcon,
|
|
||||||
ClockArrowUp,
|
|
||||||
CpuIcon,
|
|
||||||
GlobeIcon,
|
|
||||||
LayoutGridIcon,
|
|
||||||
MonitorIcon,
|
|
||||||
XIcon,
|
|
||||||
} from "lucide-react"
|
|
||||||
import { subscribeKeys } from "nanostores"
|
import { subscribeKeys } from "nanostores"
|
||||||
import React, { type JSX, lazy, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
import React, { type JSX, lazy, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||||
import AreaChartDefault, { type DataPoint } from "@/components/charts/area-chart"
|
import AreaChartDefault, { type DataPoint } from "@/components/charts/area-chart"
|
||||||
@@ -24,7 +16,7 @@ import MemChart from "@/components/charts/mem-chart"
|
|||||||
import SwapChart from "@/components/charts/swap-chart"
|
import SwapChart from "@/components/charts/swap-chart"
|
||||||
import TemperatureChart from "@/components/charts/temperature-chart"
|
import TemperatureChart from "@/components/charts/temperature-chart"
|
||||||
import { getPbTimestamp, pb } from "@/lib/api"
|
import { getPbTimestamp, pb } from "@/lib/api"
|
||||||
import { ChartType, ConnectionType, connectionTypeLabels, Os, SystemStatus, Unit } from "@/lib/enums"
|
import { ChartType, Os, SystemStatus, Unit } from "@/lib/enums"
|
||||||
import { batteryStateTranslations } from "@/lib/i18n"
|
import { batteryStateTranslations } from "@/lib/i18n"
|
||||||
import {
|
import {
|
||||||
$allSystemsById,
|
$allSystemsById,
|
||||||
@@ -44,8 +36,6 @@ import {
|
|||||||
compareSemVer,
|
compareSemVer,
|
||||||
decimalString,
|
decimalString,
|
||||||
formatBytes,
|
formatBytes,
|
||||||
secondsToString,
|
|
||||||
getHostDisplayValue,
|
|
||||||
listen,
|
listen,
|
||||||
parseSemVer,
|
parseSemVer,
|
||||||
toFixedFloat,
|
toFixedFloat,
|
||||||
@@ -61,20 +51,18 @@ import type {
|
|||||||
SystemStats,
|
SystemStats,
|
||||||
SystemStatsRecord,
|
SystemStatsRecord,
|
||||||
} from "@/types"
|
} from "@/types"
|
||||||
import ChartTimeSelect from "../charts/chart-time-select"
|
|
||||||
import { $router, navigate } from "../router"
|
import { $router, navigate } from "../router"
|
||||||
import Spinner from "../spinner"
|
import Spinner from "../spinner"
|
||||||
import { Button } from "../ui/button"
|
import { Button } from "../ui/button"
|
||||||
import { Card, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
import { Card, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
||||||
import { AppleIcon, ChartAverage, ChartMax, FreeBsdIcon, Rows, TuxIcon, WebSocketIcon, WindowsIcon } from "../ui/icons"
|
import { ChartAverage, ChartMax } from "../ui/icons"
|
||||||
import { Input } from "../ui/input"
|
import { Input } from "../ui/input"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
|
||||||
import { Separator } from "../ui/separator"
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip"
|
|
||||||
import NetworkSheet from "./system/network-sheet"
|
import NetworkSheet from "./system/network-sheet"
|
||||||
import CpuCoresSheet from "./system/cpu-sheet"
|
import CpuCoresSheet from "./system/cpu-sheet"
|
||||||
import LineChartDefault from "../charts/line-chart"
|
import LineChartDefault from "../charts/line-chart"
|
||||||
import { pinnedAxisDomain } from "../ui/chart"
|
import { pinnedAxisDomain } from "../ui/chart"
|
||||||
|
import InfoBar from "./system/info-bar"
|
||||||
|
|
||||||
type ChartTimeData = {
|
type ChartTimeData = {
|
||||||
time: number
|
time: number
|
||||||
@@ -154,8 +142,8 @@ async function getStats<T extends SystemStatsRecord | ContainerStatsRecord>(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function dockerOrPodman(str: string, system: SystemRecord): string {
|
function dockerOrPodman(str: string, isPodman: boolean): string {
|
||||||
if (system.info.p) {
|
if (isPodman) {
|
||||||
return str.replace("docker", "podman").replace("Docker", "Podman")
|
return str.replace("docker", "podman").replace("Docker", "Podman")
|
||||||
}
|
}
|
||||||
return str
|
return str
|
||||||
@@ -178,6 +166,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
const isLongerChart = !["1m", "1h"].includes(chartTime) // true if chart time is not 1m or 1h
|
const isLongerChart = !["1m", "1h"].includes(chartTime) // true if chart time is not 1m or 1h
|
||||||
const userSettings = $userSettings.get()
|
const userSettings = $userSettings.get()
|
||||||
const chartWrapRef = useRef<HTMLDivElement>(null)
|
const chartWrapRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [isPodman, setIsPodman] = useState(system.info?.p ?? false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -217,7 +206,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
// subscribe to realtime metrics if chart time is 1m
|
// subscribe to realtime metrics if chart time is 1m
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: not necessary
|
// biome-ignore lint/correctness/useExhaustiveDependencies: not necessary
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let unsub = () => { }
|
let unsub = () => {}
|
||||||
if (!system.id || chartTime !== "1m") {
|
if (!system.id || chartTime !== "1m") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -333,62 +322,9 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
})
|
})
|
||||||
}, [system, chartTime])
|
}, [system, chartTime])
|
||||||
|
|
||||||
// values for system info bar
|
useEffect(() => {
|
||||||
const systemInfo = useMemo(() => {
|
setIsPodman(system.info?.p ?? false)
|
||||||
if (!system.info) {
|
}, [system.info?.p])
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const osInfo = {
|
|
||||||
[Os.Linux]: {
|
|
||||||
Icon: TuxIcon,
|
|
||||||
value: system.info.k,
|
|
||||||
label: t({ comment: "Linux kernel", message: "Kernel" }),
|
|
||||||
},
|
|
||||||
[Os.Darwin]: {
|
|
||||||
Icon: AppleIcon,
|
|
||||||
value: `macOS ${system.info.k}`,
|
|
||||||
},
|
|
||||||
[Os.Windows]: {
|
|
||||||
Icon: WindowsIcon,
|
|
||||||
value: system.info.k,
|
|
||||||
},
|
|
||||||
[Os.FreeBSD]: {
|
|
||||||
Icon: FreeBsdIcon,
|
|
||||||
value: system.info.k,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
let uptime: string
|
|
||||||
if (system.info.u < 3600) {
|
|
||||||
uptime = secondsToString(system.info.u, "minute")
|
|
||||||
} else if (system.info.u < 360000) {
|
|
||||||
uptime = secondsToString(system.info.u, "hour")
|
|
||||||
} else {
|
|
||||||
uptime = secondsToString(system.info.u, "day")
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
{ value: getHostDisplayValue(system), Icon: GlobeIcon },
|
|
||||||
{
|
|
||||||
value: system.info.h,
|
|
||||||
Icon: MonitorIcon,
|
|
||||||
label: "Hostname",
|
|
||||||
// hide if hostname is same as host or name
|
|
||||||
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` : ""})`,
|
|
||||||
Icon: CpuIcon,
|
|
||||||
hide: !system.info.m,
|
|
||||||
},
|
|
||||||
] as {
|
|
||||||
value: string | number | undefined
|
|
||||||
label?: string
|
|
||||||
Icon: React.ElementType
|
|
||||||
hide?: boolean
|
|
||||||
}[]
|
|
||||||
}, [system, t])
|
|
||||||
|
|
||||||
/** Space for tooltip if more than 10 sensors and no containers table */
|
/** Space for tooltip if more than 10 sensors and no containers table */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -458,113 +394,11 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
const hasGpuPowerData = lastGpuVals.some((gpu) => gpu.p !== undefined || gpu.pp !== undefined)
|
const hasGpuPowerData = lastGpuVals.some((gpu) => gpu.p !== undefined || gpu.pp !== undefined)
|
||||||
const hasGpuEnginesData = lastGpuVals.some((gpu) => gpu.e !== undefined)
|
const hasGpuEnginesData = lastGpuVals.some((gpu) => gpu.e !== undefined)
|
||||||
|
|
||||||
let translatedStatus: string = system.status
|
|
||||||
if (system.status === SystemStatus.Up) {
|
|
||||||
translatedStatus = t({ message: "Up", comment: "Context: System is up" })
|
|
||||||
} else if (system.status === SystemStatus.Down) {
|
|
||||||
translatedStatus = t({ message: "Down", comment: "Context: System is down" })
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div ref={chartWrapRef} className="grid gap-4 mb-14 overflow-x-clip">
|
<div ref={chartWrapRef} className="grid gap-4 mb-14 overflow-x-clip">
|
||||||
{/* system info */}
|
{/* system info */}
|
||||||
<Card>
|
<InfoBar system={system} chartData={chartData} grid={grid} setGrid={setGrid} setIsPodman={setIsPodman} />
|
||||||
<div className="grid xl:flex gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-[1.6rem] font-semibold mb-1.5">{system.name}</h1>
|
|
||||||
<div className="flex flex-wrap items-center gap-3 gap-y-2 text-sm opacity-90">
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="capitalize flex gap-2 items-center">
|
|
||||||
<span className={cn("relative flex h-3 w-3")}>
|
|
||||||
{system.status === SystemStatus.Up && (
|
|
||||||
<span
|
|
||||||
className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
|
|
||||||
style={{ animationDuration: "1.5s" }}
|
|
||||||
></span>
|
|
||||||
)}
|
|
||||||
<span
|
|
||||||
className={cn("relative inline-flex rounded-full h-3 w-3", {
|
|
||||||
"bg-green-500": system.status === SystemStatus.Up,
|
|
||||||
"bg-red-500": system.status === SystemStatus.Down,
|
|
||||||
"bg-primary/40": system.status === SystemStatus.Paused,
|
|
||||||
"bg-yellow-500": system.status === SystemStatus.Pending,
|
|
||||||
})}
|
|
||||||
></span>
|
|
||||||
</span>
|
|
||||||
{translatedStatus}
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
{system.info.ct && (
|
|
||||||
<TooltipContent>
|
|
||||||
<div className="flex gap-1 items-center">
|
|
||||||
{system.info.ct === ConnectionType.WebSocket ? (
|
|
||||||
<WebSocketIcon className="size-4" />
|
|
||||||
) : (
|
|
||||||
<ChevronRightSquareIcon className="size-4" strokeWidth={2} />
|
|
||||||
)}
|
|
||||||
{connectionTypeLabels[system.info.ct as ConnectionType]}
|
|
||||||
</div>
|
|
||||||
</TooltipContent>
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
|
|
||||||
{systemInfo.map(({ value, label, Icon, hide }) => {
|
|
||||||
if (hide || !value) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const content = (
|
|
||||||
<div className="flex gap-1.5 items-center">
|
|
||||||
<Icon className="h-4 w-4" /> {value}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
<div key={value} className="contents">
|
|
||||||
<Separator orientation="vertical" className="h-4 bg-primary/30" />
|
|
||||||
{label ? (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip delayDuration={150}>
|
|
||||||
<TooltipTrigger asChild>{content}</TooltipTrigger>
|
|
||||||
<TooltipContent>{label}</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
) : (
|
|
||||||
content
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="xl:ms-auto flex items-center gap-2 max-sm:-mb-1">
|
|
||||||
<ChartTimeSelect className="w-full xl:w-40" agentVersion={chartData.agentVersion} />
|
|
||||||
<TooltipProvider delayDuration={100}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
aria-label={t`Toggle grid`}
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
className="hidden xl:flex p-0 text-primary"
|
|
||||||
onClick={() => setGrid(!grid)}
|
|
||||||
>
|
|
||||||
{grid ? (
|
|
||||||
<LayoutGridIcon className="h-[1.2rem] w-[1.2rem] opacity-75" />
|
|
||||||
) : (
|
|
||||||
<Rows className="h-[1.3rem] w-[1.3rem] opacity-75" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>{t`Toggle grid`}</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
|
|
||||||
{/* <Tabs defaultValue="overview" className="w-full">
|
{/* <Tabs defaultValue="overview" className="w-full">
|
||||||
<TabsList className="w-full h-11">
|
<TabsList className="w-full h-11">
|
||||||
@@ -576,7 +410,6 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs> */}
|
</Tabs> */}
|
||||||
|
|
||||||
|
|
||||||
{/* main charts */}
|
{/* main charts */}
|
||||||
<div className="grid xl:grid-cols-2 gap-4">
|
<div className="grid xl:grid-cols-2 gap-4">
|
||||||
<ChartCard
|
<ChartCard
|
||||||
@@ -612,7 +445,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
<ChartCard
|
<ChartCard
|
||||||
empty={dataEmpty}
|
empty={dataEmpty}
|
||||||
grid={grid}
|
grid={grid}
|
||||||
title={dockerOrPodman(t`Docker CPU Usage`, system)}
|
title={dockerOrPodman(t`Docker CPU Usage`, isPodman)}
|
||||||
description={t`Average CPU utilization of containers`}
|
description={t`Average CPU utilization of containers`}
|
||||||
cornerEl={containerFilterBar}
|
cornerEl={containerFilterBar}
|
||||||
>
|
>
|
||||||
@@ -639,8 +472,8 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
<ChartCard
|
<ChartCard
|
||||||
empty={dataEmpty}
|
empty={dataEmpty}
|
||||||
grid={grid}
|
grid={grid}
|
||||||
title={dockerOrPodman(t`Docker Memory Usage`, system)}
|
title={dockerOrPodman(t`Docker Memory Usage`, isPodman)}
|
||||||
description={dockerOrPodman(t`Memory usage of docker containers`, system)}
|
description={dockerOrPodman(t`Memory usage of docker containers`, isPodman)}
|
||||||
cornerEl={containerFilterBar}
|
cornerEl={containerFilterBar}
|
||||||
>
|
>
|
||||||
<ContainerChart
|
<ContainerChart
|
||||||
@@ -760,8 +593,8 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
<ChartCard
|
<ChartCard
|
||||||
empty={dataEmpty}
|
empty={dataEmpty}
|
||||||
grid={grid}
|
grid={grid}
|
||||||
title={dockerOrPodman(t`Docker Network I/O`, system)}
|
title={dockerOrPodman(t`Docker Network I/O`, isPodman)}
|
||||||
description={dockerOrPodman(t`Network traffic of docker containers`, system)}
|
description={dockerOrPodman(t`Network traffic of docker containers`, isPodman)}
|
||||||
cornerEl={containerFilterBar}
|
cornerEl={containerFilterBar}
|
||||||
>
|
>
|
||||||
<ContainerChart
|
<ContainerChart
|
||||||
@@ -800,10 +633,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
|
|
||||||
{/* Temperature chart */}
|
{/* Temperature chart */}
|
||||||
{systemStats.at(-1)?.stats.t && (
|
{systemStats.at(-1)?.stats.t && (
|
||||||
<div
|
<div ref={temperatureChartRef} className={cn("odd:last-of-type:col-span-full", { "col-span-full": !grid })}>
|
||||||
ref={temperatureChartRef}
|
|
||||||
className={cn("odd:last-of-type:col-span-full", { "col-span-full": !grid })}
|
|
||||||
>
|
|
||||||
<ChartCard
|
<ChartCard
|
||||||
empty={dataEmpty}
|
empty={dataEmpty}
|
||||||
grid={grid}
|
grid={grid}
|
||||||
@@ -965,7 +795,9 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
label: t`Write`,
|
label: t`Write`,
|
||||||
dataKey: ({ stats }) => {
|
dataKey: ({ stats }) => {
|
||||||
if (showMax) {
|
if (showMax) {
|
||||||
return stats?.efs?.[extraFsName]?.wbm || (stats?.efs?.[extraFsName]?.wm ?? 0) * 1024 * 1024
|
return (
|
||||||
|
stats?.efs?.[extraFsName]?.wbm || (stats?.efs?.[extraFsName]?.wm ?? 0) * 1024 * 1024
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return stats?.efs?.[extraFsName]?.wb || (stats?.efs?.[extraFsName]?.w ?? 0) * 1024 * 1024
|
return stats?.efs?.[extraFsName]?.wb || (stats?.efs?.[extraFsName]?.w ?? 0) * 1024 * 1024
|
||||||
},
|
},
|
||||||
@@ -1003,9 +835,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{compareSemVer(chartData.agentVersion, parseSemVer("0.15.0")) >= 0 && (
|
{compareSemVer(chartData.agentVersion, parseSemVer("0.15.0")) >= 0 && <LazySmartTable systemId={system.id} />}
|
||||||
<LazySmartTable systemId={system.id} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{containerData.length > 0 && compareSemVer(chartData.agentVersion, parseSemVer("0.14.0")) >= 0 && (
|
{containerData.length > 0 && compareSemVer(chartData.agentVersion, parseSemVer("0.14.0")) >= 0 && (
|
||||||
<LazyContainersTable systemId={system.id} />
|
<LazyContainersTable systemId={system.id} />
|
||||||
@@ -1061,13 +891,10 @@ function FilterBar({ store = $containerFilter }: { store?: typeof $containerFilt
|
|||||||
return () => clearTimeout(handle)
|
return () => clearTimeout(handle)
|
||||||
}, [inputValue, storeValue, store])
|
}, [inputValue, storeValue, store])
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
const value = e.target.value
|
||||||
const value = e.target.value
|
setInputValue(value)
|
||||||
setInputValue(value)
|
}, [])
|
||||||
},
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleClear = useCallback(() => {
|
const handleClear = useCallback(() => {
|
||||||
setInputValue("")
|
setInputValue("")
|
||||||
@@ -1194,4 +1021,4 @@ function LazySystemdTable({ systemId }: { systemId: string }) {
|
|||||||
{isIntersecting && <SystemdTable systemId={systemId} />}
|
{isIntersecting && <SystemdTable systemId={systemId} />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
259
internal/site/src/components/routes/system/info-bar.tsx
Normal file
259
internal/site/src/components/routes/system/info-bar.tsx
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
import { plural } from "@lingui/core/macro"
|
||||||
|
import { useLingui } from "@lingui/react/macro"
|
||||||
|
import {
|
||||||
|
AppleIcon,
|
||||||
|
ChevronRightSquareIcon,
|
||||||
|
ClockArrowUp,
|
||||||
|
CpuIcon,
|
||||||
|
GlobeIcon,
|
||||||
|
LayoutGridIcon,
|
||||||
|
MemoryStickIcon,
|
||||||
|
MonitorIcon,
|
||||||
|
Rows,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { useEffect, useMemo, useState } from "react"
|
||||||
|
import ChartTimeSelect from "@/components/charts/chart-time-select"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Card } from "@/components/ui/card"
|
||||||
|
import { FreeBsdIcon, TuxIcon, WebSocketIcon, WindowsIcon } from "@/components/ui/icons"
|
||||||
|
import { Separator } from "@/components/ui/separator"
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||||
|
import { pb } from "@/lib/api"
|
||||||
|
import { ConnectionType, connectionTypeLabels, Os, SystemStatus } from "@/lib/enums"
|
||||||
|
import { cn, formatBytes, getHostDisplayValue, secondsToString, toFixedFloat } from "@/lib/utils"
|
||||||
|
import type { ChartData, SystemDetailsRecord, SystemRecord } from "@/types"
|
||||||
|
|
||||||
|
export default function InfoBar({
|
||||||
|
system,
|
||||||
|
chartData,
|
||||||
|
grid,
|
||||||
|
setGrid,
|
||||||
|
setIsPodman,
|
||||||
|
}: {
|
||||||
|
system: SystemRecord
|
||||||
|
chartData: ChartData
|
||||||
|
grid: boolean
|
||||||
|
setGrid: (grid: boolean) => void
|
||||||
|
setIsPodman: (isPodman: boolean) => void
|
||||||
|
}) {
|
||||||
|
const { t } = useLingui()
|
||||||
|
const [details, setDetails] = useState<SystemDetailsRecord | null>(null)
|
||||||
|
|
||||||
|
// Fetch system_details on mount / when system changes
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true
|
||||||
|
setDetails(null)
|
||||||
|
// skip fetching system details if agent is older version which includes details in Info struct
|
||||||
|
if (!system.id || system.info?.m) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pb.collection<SystemDetailsRecord>("system_details")
|
||||||
|
.getOne(system.id, {
|
||||||
|
fields: "hostname,kernel,cores,threads,cpu,os,os_name,arch,memory,podman",
|
||||||
|
headers: {
|
||||||
|
"Cache-Control": "public, max-age=60",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((details) => {
|
||||||
|
if (active) {
|
||||||
|
setDetails(details)
|
||||||
|
setIsPodman(details.podman)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
active = false
|
||||||
|
}
|
||||||
|
}, [system.id])
|
||||||
|
|
||||||
|
// values for system info bar - use details with fallback to system.info
|
||||||
|
const systemInfo = useMemo(() => {
|
||||||
|
if (!system.info) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use details if available, otherwise fall back to system.info
|
||||||
|
const hostname = details?.hostname ?? system.info.h
|
||||||
|
const kernel = details?.kernel ?? system.info.k
|
||||||
|
const cores = details?.cores ?? system.info.c
|
||||||
|
const threads = details?.threads ?? system.info.t ?? 0
|
||||||
|
const cpuModel = details?.cpu ?? system.info.m
|
||||||
|
const os = details?.os ?? system.info.os ?? Os.Linux
|
||||||
|
const osName = details?.os_name
|
||||||
|
const arch = details?.arch
|
||||||
|
const memory = details?.memory
|
||||||
|
|
||||||
|
const osInfo = {
|
||||||
|
[Os.Linux]: {
|
||||||
|
Icon: TuxIcon,
|
||||||
|
// show kernel in tooltip if os name is available, otherwise show the kernel
|
||||||
|
value: osName || kernel,
|
||||||
|
label: osName ? kernel : undefined,
|
||||||
|
},
|
||||||
|
[Os.Darwin]: {
|
||||||
|
Icon: AppleIcon,
|
||||||
|
value: osName || `macOS ${kernel}`,
|
||||||
|
},
|
||||||
|
[Os.Windows]: {
|
||||||
|
Icon: WindowsIcon,
|
||||||
|
value: osName || kernel,
|
||||||
|
label: osName ? kernel : undefined,
|
||||||
|
},
|
||||||
|
[Os.FreeBSD]: {
|
||||||
|
Icon: FreeBsdIcon,
|
||||||
|
value: osName || kernel,
|
||||||
|
label: osName ? kernel : undefined,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
let uptime: string
|
||||||
|
if (system.info.u < 3600) {
|
||||||
|
uptime = secondsToString(system.info.u, "minute")
|
||||||
|
} else if (system.info.u < 360000) {
|
||||||
|
uptime = secondsToString(system.info.u, "hour")
|
||||||
|
} else {
|
||||||
|
uptime = secondsToString(system.info.u, "day")
|
||||||
|
}
|
||||||
|
const info = [
|
||||||
|
{ value: getHostDisplayValue(system), Icon: GlobeIcon },
|
||||||
|
{
|
||||||
|
value: hostname,
|
||||||
|
Icon: MonitorIcon,
|
||||||
|
label: "Hostname",
|
||||||
|
// hide if hostname is same as host or name
|
||||||
|
hide: hostname === system.host || hostname === system.name,
|
||||||
|
},
|
||||||
|
{ value: uptime, Icon: ClockArrowUp, label: t`Uptime`, hide: !system.info.u },
|
||||||
|
osInfo[os],
|
||||||
|
{
|
||||||
|
value: cpuModel,
|
||||||
|
Icon: CpuIcon,
|
||||||
|
hide: !cpuModel,
|
||||||
|
label: `${plural(cores, { one: "# core", other: "# cores" })} / ${plural(threads, { one: "# thread", other: "# threads" })}${arch ? ` / ${arch}` : ""}`,
|
||||||
|
},
|
||||||
|
] as {
|
||||||
|
value: string | number | undefined
|
||||||
|
label?: string
|
||||||
|
Icon: React.ElementType
|
||||||
|
hide?: boolean
|
||||||
|
}[]
|
||||||
|
|
||||||
|
if (memory) {
|
||||||
|
const memValue = formatBytes(memory, false, undefined, false)
|
||||||
|
info.push({
|
||||||
|
value: `${toFixedFloat(memValue.value, memValue.value >= 10 ? 1 : 2)} ${memValue.unit}`,
|
||||||
|
Icon: MemoryStickIcon,
|
||||||
|
hide: !memory,
|
||||||
|
label: t`Memory`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}, [system, details, t])
|
||||||
|
|
||||||
|
let translatedStatus: string = system.status
|
||||||
|
if (system.status === SystemStatus.Up) {
|
||||||
|
translatedStatus = t({ message: "Up", comment: "Context: System is up" })
|
||||||
|
} else if (system.status === SystemStatus.Down) {
|
||||||
|
translatedStatus = t({ message: "Down", comment: "Context: System is down" })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<div className="grid xl:flex gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-[1.6rem] font-semibold mb-1.5">{system.name}</h1>
|
||||||
|
<div className="flex flex-wrap items-center gap-3 gap-y-2 text-sm opacity-90">
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="capitalize flex gap-2 items-center">
|
||||||
|
<span className={cn("relative flex h-3 w-3")}>
|
||||||
|
{system.status === SystemStatus.Up && (
|
||||||
|
<span
|
||||||
|
className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
|
||||||
|
style={{ animationDuration: "1.5s" }}
|
||||||
|
></span>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={cn("relative inline-flex rounded-full h-3 w-3", {
|
||||||
|
"bg-green-500": system.status === SystemStatus.Up,
|
||||||
|
"bg-red-500": system.status === SystemStatus.Down,
|
||||||
|
"bg-primary/40": system.status === SystemStatus.Paused,
|
||||||
|
"bg-yellow-500": system.status === SystemStatus.Pending,
|
||||||
|
})}
|
||||||
|
></span>
|
||||||
|
</span>
|
||||||
|
{translatedStatus}
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
{system.info.ct && (
|
||||||
|
<TooltipContent>
|
||||||
|
<div className="flex gap-1 items-center">
|
||||||
|
{system.info.ct === ConnectionType.WebSocket ? (
|
||||||
|
<WebSocketIcon className="size-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronRightSquareIcon className="size-4" strokeWidth={2} />
|
||||||
|
)}
|
||||||
|
{connectionTypeLabels[system.info.ct as ConnectionType]}
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
{systemInfo.map(({ value, label, Icon, hide }) => {
|
||||||
|
if (hide || !value) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const content = (
|
||||||
|
<div className="flex gap-1.5 items-center">
|
||||||
|
<Icon className="h-4 w-4" /> {value}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<div key={value} className="contents">
|
||||||
|
<Separator orientation="vertical" className="h-4 bg-primary/30" />
|
||||||
|
{label ? (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip delayDuration={150}>
|
||||||
|
<TooltipTrigger asChild>{content}</TooltipTrigger>
|
||||||
|
<TooltipContent>{label}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
) : (
|
||||||
|
content
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="xl:ms-auto flex items-center gap-2 max-sm:-mb-1">
|
||||||
|
<ChartTimeSelect className="w-full xl:w-40" agentVersion={chartData.agentVersion} />
|
||||||
|
<TooltipProvider delayDuration={100}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
aria-label={t`Toggle grid`}
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="hidden xl:flex p-0 text-primary"
|
||||||
|
onClick={() => setGrid(!grid)}
|
||||||
|
>
|
||||||
|
{grid ? (
|
||||||
|
<LayoutGridIcon className="h-[1.2rem] w-[1.2rem] opacity-75" />
|
||||||
|
) : (
|
||||||
|
<Rows className="h-[1.3rem] w-[1.3rem] opacity-75" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{t`Toggle grid`}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
13
internal/site/src/types.d.ts
vendored
13
internal/site/src/types.d.ts
vendored
@@ -380,6 +380,19 @@ export interface SmartAttribute {
|
|||||||
wf?: string
|
wf?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SystemDetailsRecord extends RecordModel {
|
||||||
|
system: string
|
||||||
|
hostname: string
|
||||||
|
kernel: string
|
||||||
|
cores: number
|
||||||
|
threads: number
|
||||||
|
cpu: string
|
||||||
|
os: Os
|
||||||
|
os_name: string
|
||||||
|
memory: number
|
||||||
|
podman: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface SmartDeviceRecord extends RecordModel {
|
export interface SmartDeviceRecord extends RecordModel {
|
||||||
id: string
|
id: string
|
||||||
system: string
|
system: string
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
## 0.18.0
|
||||||
|
|
||||||
|
- Remove `la1`, `la5`, `la15` fields from `Info` struct in favor of `la` array.
|
||||||
|
|
||||||
|
- Remove `MB` bandwidth values in favor of bytes.
|
||||||
|
|
||||||
## 0.17.0
|
## 0.17.0
|
||||||
|
|
||||||
- Add quiet hours to silence alerts during specific time periods. (#265)
|
- Add quiet hours to silence alerts during specific time periods. (#265)
|
||||||
|
|||||||
Reference in New Issue
Block a user