mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-23 22:16:18 +01:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca58ff66ba | ||
|
|
133d229361 | ||
|
|
960cac4060 | ||
|
|
d83865cb4f | ||
|
|
4b43d68da6 | ||
|
|
c790d76211 | ||
|
|
29b182fd7b | ||
|
|
fc78b959aa | ||
|
|
b8b3604aec | ||
|
|
e45606fdec | ||
|
|
640afd82ad | ||
|
|
d025e51c67 | ||
|
|
f70c30345a | ||
|
|
63bdac83a1 | ||
|
|
65897a8df6 | ||
|
|
0dc9b3e273 | ||
|
|
c1c0d8d672 | ||
|
|
1811ab64be | ||
|
|
5578520054 | ||
|
|
7b128d09ac | ||
|
|
d295507c0b | ||
|
|
79fbbb7ad0 | ||
|
|
e7325b23c4 | ||
|
|
c5eba6547a |
8
.github/workflows/docker-images.yml
vendored
8
.github/workflows/docker-images.yml
vendored
@@ -64,6 +64,14 @@ jobs:
|
|||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password_secret: GITHUB_TOKEN
|
password_secret: GITHUB_TOKEN
|
||||||
|
|
||||||
|
- image: ghcr.io/${{ github.repository }}/beszel-agent-intel
|
||||||
|
context: ./
|
||||||
|
dockerfile: ./internal/dockerfile_agent_intel
|
||||||
|
platforms: linux/amd64
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password_secret: GITHUB_TOKEN
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|||||||
@@ -258,6 +258,7 @@ func (gm *GPUManager) GetCurrentData() map[string]system.GPUData {
|
|||||||
gpuAvg.Engines[name] = twoDecimals(engine / count)
|
gpuAvg.Engines[name] = twoDecimals(engine / count)
|
||||||
maxEngineUsage = max(maxEngineUsage, engine/count)
|
maxEngineUsage = max(maxEngineUsage, engine/count)
|
||||||
}
|
}
|
||||||
|
gpuAvg.PowerPkg = twoDecimals(gpu.PowerPkg / count)
|
||||||
gpuAvg.Usage = twoDecimals(maxEngineUsage)
|
gpuAvg.Usage = twoDecimals(maxEngineUsage)
|
||||||
} else {
|
} else {
|
||||||
gpuAvg.Usage = twoDecimals(gpu.Usage / count)
|
gpuAvg.Usage = twoDecimals(gpu.Usage / count)
|
||||||
@@ -266,7 +267,7 @@ func (gm *GPUManager) GetCurrentData() map[string]system.GPUData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// reset accumulators in the original gpu data for next collection
|
// reset accumulators in the original gpu data for next collection
|
||||||
gpu.Usage, gpu.Power, gpu.Count = gpuAvg.Usage, gpuAvg.Power, 1
|
gpu.Usage, gpu.Power, gpu.PowerPkg, gpu.Count = gpuAvg.Usage, gpuAvg.Power, gpuAvg.PowerPkg, 1
|
||||||
gpu.Engines = gpuAvg.Engines
|
gpu.Engines = gpuAvg.Engines
|
||||||
|
|
||||||
// append id to the name if there are multiple GPUs with the same name
|
// append id to the name if there are multiple GPUs with the same name
|
||||||
@@ -358,6 +359,9 @@ func (gm *GPUManager) startCollector(command string) {
|
|||||||
|
|
||||||
// NewGPUManager creates and initializes a new GPUManager
|
// NewGPUManager creates and initializes a new GPUManager
|
||||||
func NewGPUManager() (*GPUManager, error) {
|
func NewGPUManager() (*GPUManager, error) {
|
||||||
|
if skipGPU, _ := GetEnv("SKIP_GPU"); skipGPU == "true" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
var gm GPUManager
|
var gm GPUManager
|
||||||
if err := gm.detectGPUs(); err != nil {
|
if err := gm.detectGPUs(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package agent
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"bufio"
|
||||||
"fmt"
|
"io"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/internal/entities/system"
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
)
|
)
|
||||||
@@ -14,12 +16,9 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type intelGpuStats struct {
|
type intelGpuStats struct {
|
||||||
Power struct {
|
PowerGPU float64
|
||||||
GPU float64 `json:"GPU"`
|
PowerPkg float64
|
||||||
} `json:"power"`
|
Engines map[string]float64
|
||||||
Engines map[string]struct {
|
|
||||||
Busy float64 `json:"busy"`
|
|
||||||
} `json:"engines"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateIntelFromStats updates aggregated GPU data from a single intelGpuStats sample
|
// updateIntelFromStats updates aggregated GPU data from a single intelGpuStats sample
|
||||||
@@ -34,24 +33,25 @@ func (gm *GPUManager) updateIntelFromStats(sample *intelGpuStats) bool {
|
|||||||
gm.GpuDataMap["0"] = gpuData
|
gm.GpuDataMap["0"] = gpuData
|
||||||
}
|
}
|
||||||
|
|
||||||
if sample.Power.GPU > 0 {
|
gpuData.Power += sample.PowerGPU
|
||||||
gpuData.Power += sample.Power.GPU
|
gpuData.PowerPkg += sample.PowerPkg
|
||||||
}
|
|
||||||
|
|
||||||
if gpuData.Engines == nil {
|
if gpuData.Engines == nil {
|
||||||
gpuData.Engines = make(map[string]float64, len(sample.Engines))
|
gpuData.Engines = make(map[string]float64, len(sample.Engines))
|
||||||
}
|
}
|
||||||
for name, engine := range sample.Engines {
|
for name, engine := range sample.Engines {
|
||||||
gpuData.Engines[name] += engine.Busy
|
gpuData.Engines[name] += engine
|
||||||
}
|
}
|
||||||
|
|
||||||
gpuData.Count++
|
gpuData.Count++
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectIntelStats executes intel_gpu_top in JSON mode and stream-decodes the array of samples
|
// collectIntelStats executes intel_gpu_top in text mode (-l) and parses the output
|
||||||
func (gm *GPUManager) collectIntelStats() error {
|
func (gm *GPUManager) collectIntelStats() (err error) {
|
||||||
cmd := exec.Command(intelGpuStatsCmd, "-s", intelGpuStatsInterval, "-J")
|
cmd := exec.Command(intelGpuStatsCmd, "-s", intelGpuStatsInterval, "-l")
|
||||||
|
// Avoid blocking if intel_gpu_top writes to stderr
|
||||||
|
cmd.Stderr = io.Discard
|
||||||
stdout, err := cmd.StdoutPipe()
|
stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -60,43 +60,140 @@ func (gm *GPUManager) collectIntelStats() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
dec := json.NewDecoder(stdout)
|
// Ensure we always reap the child to avoid zombies on any return path and
|
||||||
|
// propagate a non-zero exit code if no other error was set.
|
||||||
|
defer func() {
|
||||||
|
// Best-effort close of the pipe (unblock the child if it writes)
|
||||||
|
_ = stdout.Close()
|
||||||
|
if cmd.ProcessState == nil || !cmd.ProcessState.Exited() {
|
||||||
|
_ = cmd.Process.Kill()
|
||||||
|
}
|
||||||
|
if waitErr := cmd.Wait(); err == nil && waitErr != nil {
|
||||||
|
err = waitErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Expect a JSON array stream: [ { ... }, { ... }, ... ]
|
scanner := bufio.NewScanner(stdout)
|
||||||
tok, err := dec.Token()
|
var header1 string
|
||||||
if err != nil {
|
var engineNames []string
|
||||||
return err
|
var friendlyNames []string
|
||||||
}
|
var preEngineCols int
|
||||||
if delim, ok := tok.(json.Delim); !ok || delim != '[' {
|
var powerIndex int
|
||||||
return fmt.Errorf("unexpected JSON start token: %v", tok)
|
var hadDataRow bool
|
||||||
}
|
// skip first data row because it sometimes has erroneous data
|
||||||
|
var skippedFirstDataRow bool
|
||||||
|
|
||||||
var sample intelGpuStats
|
for scanner.Scan() {
|
||||||
for {
|
line := strings.TrimSpace(scanner.Text())
|
||||||
if dec.More() {
|
if line == "" {
|
||||||
// Clear the engines map before decoding
|
|
||||||
if sample.Engines != nil {
|
|
||||||
for k := range sample.Engines {
|
|
||||||
delete(sample.Engines, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := dec.Decode(&sample); err != nil {
|
|
||||||
return fmt.Errorf("decode intel gpu: %w", err)
|
|
||||||
}
|
|
||||||
gm.updateIntelFromStats(&sample)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Attempt to read closing bracket (will only be present when process exits)
|
|
||||||
tok, err = dec.Token()
|
// first header line
|
||||||
|
if strings.HasPrefix(line, "Freq") {
|
||||||
|
header1 = line
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// second header line
|
||||||
|
if strings.HasPrefix(line, "req") {
|
||||||
|
engineNames, friendlyNames, powerIndex, preEngineCols = gm.parseIntelHeaders(header1, line)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data row
|
||||||
|
if !skippedFirstDataRow {
|
||||||
|
skippedFirstDataRow = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sample, err := gm.parseIntelData(line, engineNames, friendlyNames, powerIndex, preEngineCols)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// When the process is still running, decoder will block in More/Decode; any error here is terminal
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if delim, ok := tok.(json.Delim); ok && delim == ']' {
|
hadDataRow = true
|
||||||
break
|
gm.updateIntelFromStats(&sample)
|
||||||
|
}
|
||||||
|
if scanErr := scanner.Err(); scanErr != nil {
|
||||||
|
return scanErr
|
||||||
|
}
|
||||||
|
if !hadDataRow {
|
||||||
|
return errNoValidData
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gm *GPUManager) parseIntelHeaders(header1 string, header2 string) (engineNames []string, friendlyNames []string, powerIndex int, preEngineCols int) {
|
||||||
|
// Build indexes
|
||||||
|
h1 := strings.Fields(header1)
|
||||||
|
h2 := strings.Fields(header2)
|
||||||
|
powerIndex = -1 // Initialize to -1, will be set to actual index if found
|
||||||
|
// Collect engine names from header1
|
||||||
|
for _, col := range h1 {
|
||||||
|
key := strings.TrimRightFunc(col, func(r rune) bool { return r >= '0' && r <= '9' })
|
||||||
|
var friendly string
|
||||||
|
switch key {
|
||||||
|
case "RCS":
|
||||||
|
friendly = "Render/3D"
|
||||||
|
case "BCS":
|
||||||
|
friendly = "Blitter"
|
||||||
|
case "VCS":
|
||||||
|
friendly = "Video"
|
||||||
|
case "VECS":
|
||||||
|
friendly = "VideoEnhance"
|
||||||
|
case "CCS":
|
||||||
|
friendly = "Compute"
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
engineNames = append(engineNames, key)
|
||||||
|
friendlyNames = append(friendlyNames, friendly)
|
||||||
|
}
|
||||||
|
// find power gpu index among pre-engine columns
|
||||||
|
if n := len(engineNames); n > 0 {
|
||||||
|
preEngineCols = max(len(h2)-3*n, 0)
|
||||||
|
limit := min(len(h2), preEngineCols)
|
||||||
|
for i := range limit {
|
||||||
|
if strings.EqualFold(h2[i], "gpu") {
|
||||||
|
powerIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return engineNames, friendlyNames, powerIndex, preEngineCols
|
||||||
return cmd.Wait()
|
}
|
||||||
|
|
||||||
|
func (gm *GPUManager) parseIntelData(line string, engineNames []string, friendlyNames []string, powerIndex int, preEngineCols int) (sample intelGpuStats, err error) {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) == 0 {
|
||||||
|
return sample, errNoValidData
|
||||||
|
}
|
||||||
|
// Make sure row has enough columns for engines
|
||||||
|
if need := preEngineCols + 3*len(engineNames); len(fields) < need {
|
||||||
|
return sample, errNoValidData
|
||||||
|
}
|
||||||
|
if powerIndex >= 0 && powerIndex < len(fields) {
|
||||||
|
if v, perr := strconv.ParseFloat(fields[powerIndex], 64); perr == nil {
|
||||||
|
sample.PowerGPU = v
|
||||||
|
}
|
||||||
|
if v, perr := strconv.ParseFloat(fields[powerIndex+1], 64); perr == nil {
|
||||||
|
sample.PowerPkg = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(engineNames) > 0 {
|
||||||
|
sample.Engines = make(map[string]float64, len(engineNames))
|
||||||
|
for k := range engineNames {
|
||||||
|
base := preEngineCols + 3*k
|
||||||
|
if base < len(fields) {
|
||||||
|
busy := 0.0
|
||||||
|
if v, e := strconv.ParseFloat(fields[base], 64); e == nil {
|
||||||
|
busy = v
|
||||||
|
}
|
||||||
|
cur := sample.Engines[friendlyNames[k]]
|
||||||
|
sample.Engines[friendlyNames[k]] = cur + busy
|
||||||
|
} else {
|
||||||
|
sample.Engines[friendlyNames[k]] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sample, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -756,11 +756,11 @@ func TestAccumulation(t *testing.T) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.InDelta(t, expected.temperature, gpu.Temperature, 0.01, "Temperature should match")
|
assert.EqualValues(t, expected.temperature, gpu.Temperature, "Temperature should match")
|
||||||
assert.InDelta(t, expected.memoryUsed, gpu.MemoryUsed, 0.01, "Memory used should match")
|
assert.EqualValues(t, expected.memoryUsed, gpu.MemoryUsed, "Memory used should match")
|
||||||
assert.InDelta(t, expected.memoryTotal, gpu.MemoryTotal, 0.01, "Memory total should match")
|
assert.EqualValues(t, expected.memoryTotal, gpu.MemoryTotal, "Memory total should match")
|
||||||
assert.InDelta(t, expected.usage, gpu.Usage, 0.01, "Usage should match")
|
assert.EqualValues(t, expected.usage, gpu.Usage, "Usage should match")
|
||||||
assert.InDelta(t, expected.power, gpu.Power, 0.01, "Power should match")
|
assert.EqualValues(t, expected.power, gpu.Power, "Power should match")
|
||||||
assert.Equal(t, expected.count, gpu.Count, "Count should match")
|
assert.Equal(t, expected.count, gpu.Count, "Count should match")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -773,9 +773,9 @@ func TestAccumulation(t *testing.T) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.InDelta(t, expected.temperature, gpu.Temperature, 0.01, "Temperature in GetCurrentData should match")
|
assert.EqualValues(t, expected.temperature, gpu.Temperature, "Temperature in GetCurrentData should match")
|
||||||
assert.InDelta(t, expected.avgUsage, gpu.Usage, 0.01, "Average usage in GetCurrentData should match")
|
assert.EqualValues(t, expected.avgUsage, gpu.Usage, "Average usage in GetCurrentData should match")
|
||||||
assert.InDelta(t, expected.avgPower, gpu.Power, 0.01, "Average power in GetCurrentData should match")
|
assert.EqualValues(t, expected.avgPower, gpu.Power, "Average power in GetCurrentData should match")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify that accumulators in the original map are reset
|
// Verify that accumulators in the original map are reset
|
||||||
@@ -800,14 +800,12 @@ func TestIntelUpdateFromStats(t *testing.T) {
|
|||||||
|
|
||||||
// First sample with power and two engines
|
// First sample with power and two engines
|
||||||
sample1 := intelGpuStats{
|
sample1 := intelGpuStats{
|
||||||
Engines: map[string]struct {
|
PowerGPU: 10.5,
|
||||||
Busy float64 `json:"busy"`
|
Engines: map[string]float64{
|
||||||
}{
|
"Render/3D": 20.0,
|
||||||
"Render/3D": {Busy: 20.0},
|
"Video": 5.0,
|
||||||
"Video": {Busy: 5.0},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
sample1.Power.GPU = 10.5
|
|
||||||
|
|
||||||
ok := gm.updateIntelFromStats(&sample1)
|
ok := gm.updateIntelFromStats(&sample1)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
@@ -815,33 +813,31 @@ func TestIntelUpdateFromStats(t *testing.T) {
|
|||||||
gpu := gm.GpuDataMap["0"]
|
gpu := gm.GpuDataMap["0"]
|
||||||
require.NotNil(t, gpu)
|
require.NotNil(t, gpu)
|
||||||
assert.Equal(t, "GPU", gpu.Name)
|
assert.Equal(t, "GPU", gpu.Name)
|
||||||
assert.InDelta(t, 10.5, gpu.Power, 0.001)
|
assert.EqualValues(t, 10.5, gpu.Power)
|
||||||
assert.InDelta(t, 20.0, gpu.Engines["Render/3D"], 0.001)
|
assert.EqualValues(t, 20.0, gpu.Engines["Render/3D"])
|
||||||
assert.InDelta(t, 5.0, gpu.Engines["Video"], 0.001)
|
assert.EqualValues(t, 5.0, gpu.Engines["Video"])
|
||||||
assert.Equal(t, float64(1), gpu.Count)
|
assert.Equal(t, float64(1), gpu.Count)
|
||||||
|
|
||||||
// Second sample with zero power (should not add) and additional engine busy
|
// Second sample with zero power (should not add) and additional engine busy
|
||||||
sample2 := intelGpuStats{
|
sample2 := intelGpuStats{
|
||||||
Engines: map[string]struct {
|
PowerGPU: 0.0,
|
||||||
Busy float64 `json:"busy"`
|
Engines: map[string]float64{
|
||||||
}{
|
"Render/3D": 10.0,
|
||||||
"Render/3D": {Busy: 10.0},
|
"Video": 2.5,
|
||||||
"Video": {Busy: 2.5},
|
"Blitter": 1.0,
|
||||||
"Blitter": {Busy: 1.0},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
// zero power should not increment power accumulator
|
// zero power should not increment power accumulator
|
||||||
sample2.Power.GPU = 0.0
|
|
||||||
|
|
||||||
ok = gm.updateIntelFromStats(&sample2)
|
ok = gm.updateIntelFromStats(&sample2)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
|
|
||||||
gpu = gm.GpuDataMap["0"]
|
gpu = gm.GpuDataMap["0"]
|
||||||
require.NotNil(t, gpu)
|
require.NotNil(t, gpu)
|
||||||
assert.InDelta(t, 10.5, gpu.Power, 0.001)
|
assert.EqualValues(t, 10.5, gpu.Power)
|
||||||
assert.InDelta(t, 30.0, gpu.Engines["Render/3D"], 0.001) // 20 + 10
|
assert.EqualValues(t, 30.0, gpu.Engines["Render/3D"]) // 20 + 10
|
||||||
assert.InDelta(t, 7.5, gpu.Engines["Video"], 0.001) // 5 + 2.5
|
assert.EqualValues(t, 7.5, gpu.Engines["Video"]) // 5 + 2.5
|
||||||
assert.InDelta(t, 1.0, gpu.Engines["Blitter"], 0.001)
|
assert.EqualValues(t, 1.0, gpu.Engines["Blitter"])
|
||||||
assert.Equal(t, float64(2), gpu.Count)
|
assert.Equal(t, float64(2), gpu.Count)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -853,15 +849,15 @@ func TestIntelCollectorStreaming(t *testing.T) {
|
|||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
os.Setenv("PATH", dir)
|
os.Setenv("PATH", dir)
|
||||||
|
|
||||||
// Create a fake intel_gpu_top that prints a JSON array with two samples and exits
|
// Create a fake intel_gpu_top that prints -l format with four samples (first will be skipped) and exits
|
||||||
scriptPath := filepath.Join(dir, "intel_gpu_top")
|
scriptPath := filepath.Join(dir, "intel_gpu_top")
|
||||||
script := `#!/bin/sh
|
script := `#!/bin/sh
|
||||||
# Ignore args -s and -J
|
echo "Freq MHz IRQ RC6 Power W IMC MiB/s RCS BCS VCS"
|
||||||
# Emit a JSON array with two objects, separated by a comma, then exit
|
echo " req act /s % gpu pkg rd wr % se wa % se wa % se wa"
|
||||||
(echo '['; \
|
echo "373 373 224 45 1.50 4.13 2554 714 12.34 0 0 0.00 0 0 5.00 0 0"
|
||||||
echo '{"power":{"GPU":1.5},"engines":{"Render/3D":{"busy":12.34}}},'; \
|
echo "226 223 338 58 2.00 2.69 1820 965 0.00 0 0 0.00 0 0 0.00 0 0"
|
||||||
echo '{"power":{"GPU":2.0},"engines":{"Video":{"busy":5}}}'; \
|
echo "189 187 412 67 1.80 2.45 1950 823 8.50 2 1 15.00 1 0 22.00 0 1"
|
||||||
echo ']')`
|
echo "298 295 278 51 2.20 3.12 1675 942 5.75 1 2 9.50 3 1 12.00 1 0"`
|
||||||
if err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil {
|
if err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -870,18 +866,227 @@ func TestIntelCollectorStreaming(t *testing.T) {
|
|||||||
GpuDataMap: make(map[string]*system.GPUData),
|
GpuDataMap: make(map[string]*system.GPUData),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the collector once; it should read two samples and return
|
// Run the collector once; it should read four samples but skip the first and return
|
||||||
if err := gm.collectIntelStats(); err != nil {
|
if err := gm.collectIntelStats(); err != nil {
|
||||||
t.Fatalf("collectIntelStats error: %v", err)
|
t.Fatalf("collectIntelStats error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gpu := gm.GpuDataMap["0"]
|
gpu := gm.GpuDataMap["0"]
|
||||||
require.NotNil(t, gpu)
|
require.NotNil(t, gpu)
|
||||||
// Power should be sum of non-zero samples: 1.5 + 2.0 = 3.5
|
// Power should be sum of samples 2-4 (first is skipped): 2.0 + 1.8 + 2.2 = 6.0
|
||||||
assert.InDelta(t, 3.5, gpu.Power, 0.001)
|
assert.EqualValues(t, 6.0, gpu.Power)
|
||||||
// Engines aggregated
|
assert.InDelta(t, 8.26, gpu.PowerPkg, 0.01) // Allow small floating point differences
|
||||||
assert.InDelta(t, 12.34, gpu.Engines["Render/3D"], 0.001)
|
// Engines aggregated from samples 2-4
|
||||||
assert.InDelta(t, 5.0, gpu.Engines["Video"], 0.001)
|
assert.EqualValues(t, 14.25, gpu.Engines["Render/3D"]) // 0.00 + 8.50 + 5.75
|
||||||
// Count should be 2 samples
|
assert.EqualValues(t, 34.0, gpu.Engines["Video"]) // 0.00 + 22.00 + 12.00
|
||||||
assert.Equal(t, float64(2), gpu.Count)
|
assert.EqualValues(t, 24.5, gpu.Engines["Blitter"]) // 0.00 + 15.00 + 9.50
|
||||||
|
// Count should be 3 samples (first is skipped)
|
||||||
|
assert.Equal(t, float64(3), gpu.Count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseIntelHeaders(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
header1 string
|
||||||
|
header2 string
|
||||||
|
wantEngineNames []string
|
||||||
|
wantFriendlyNames []string
|
||||||
|
wantPowerIndex int
|
||||||
|
wantPreEngineCols int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "basic headers with RCS BCS VCS",
|
||||||
|
header1: "Freq MHz IRQ RC6 Power W IMC MiB/s RCS BCS VCS",
|
||||||
|
header2: " req act /s % gpu pkg rd wr % se wa % se wa % se wa",
|
||||||
|
wantEngineNames: []string{"RCS", "BCS", "VCS"},
|
||||||
|
wantFriendlyNames: []string{"Render/3D", "Blitter", "Video"},
|
||||||
|
wantPowerIndex: 4, // "gpu" is at index 4
|
||||||
|
wantPreEngineCols: 8, // 17 total cols - 3*3 = 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "headers with only RCS",
|
||||||
|
header1: "Freq MHz IRQ RC6 Power W IMC MiB/s RCS",
|
||||||
|
header2: " req act /s % gpu pkg rd wr % se wa",
|
||||||
|
wantEngineNames: []string{"RCS"},
|
||||||
|
wantFriendlyNames: []string{"Render/3D"},
|
||||||
|
wantPowerIndex: 4,
|
||||||
|
wantPreEngineCols: 8, // 11 total - 3*1 = 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "headers with VECS and CCS",
|
||||||
|
header1: "Freq MHz IRQ RC6 Power W IMC MiB/s VECS CCS",
|
||||||
|
header2: " req act /s % gpu pkg rd wr % se wa % se wa",
|
||||||
|
wantEngineNames: []string{"VECS", "CCS"},
|
||||||
|
wantFriendlyNames: []string{"VideoEnhance", "Compute"},
|
||||||
|
wantPowerIndex: 4,
|
||||||
|
wantPreEngineCols: 8, // 14 total - 3*2 = 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no engines",
|
||||||
|
header1: "Freq MHz IRQ RC6 Power W IMC MiB/s",
|
||||||
|
header2: " req act /s % gpu pkg rd wr",
|
||||||
|
wantEngineNames: nil, // no engines found, slices remain nil
|
||||||
|
wantFriendlyNames: nil,
|
||||||
|
wantPowerIndex: -1, // no engines, so no search
|
||||||
|
wantPreEngineCols: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "power index not found",
|
||||||
|
header1: "Freq MHz IRQ RC6 Power W IMC MiB/s RCS",
|
||||||
|
header2: " req act /s % pkg cpu rd wr % se wa", // no "gpu"
|
||||||
|
wantEngineNames: []string{"RCS"},
|
||||||
|
wantFriendlyNames: []string{"Render/3D"},
|
||||||
|
wantPowerIndex: -1, // "gpu" not found
|
||||||
|
wantPreEngineCols: 8, // 11 total - 3*1 = 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty headers",
|
||||||
|
header1: "",
|
||||||
|
header2: "",
|
||||||
|
wantEngineNames: nil, // empty input, slices remain nil
|
||||||
|
wantFriendlyNames: nil,
|
||||||
|
wantPowerIndex: -1,
|
||||||
|
wantPreEngineCols: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
gm := &GPUManager{}
|
||||||
|
engineNames, friendlyNames, powerIndex, preEngineCols := gm.parseIntelHeaders(tt.header1, tt.header2)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.wantEngineNames, engineNames)
|
||||||
|
assert.Equal(t, tt.wantFriendlyNames, friendlyNames)
|
||||||
|
assert.Equal(t, tt.wantPowerIndex, powerIndex)
|
||||||
|
assert.Equal(t, tt.wantPreEngineCols, preEngineCols)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseIntelData(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
line string
|
||||||
|
engineNames []string
|
||||||
|
friendlyNames []string
|
||||||
|
powerIndex int
|
||||||
|
preEngineCols int
|
||||||
|
wantPowerGPU float64
|
||||||
|
wantEngines map[string]float64
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "basic data with power and engines",
|
||||||
|
line: "373 373 224 45 1.50 4.13 2554 714 12.34 0 0 0.00 0 0 5.00 0 0",
|
||||||
|
engineNames: []string{"RCS", "BCS", "VCS"},
|
||||||
|
friendlyNames: []string{"Render/3D", "Blitter", "Video"},
|
||||||
|
powerIndex: 4,
|
||||||
|
preEngineCols: 8,
|
||||||
|
wantPowerGPU: 1.50,
|
||||||
|
wantEngines: map[string]float64{
|
||||||
|
"Render/3D": 12.34,
|
||||||
|
"Blitter": 0.00,
|
||||||
|
"Video": 5.00,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "data with zero power",
|
||||||
|
line: "226 223 338 58 0.00 2.69 1820 965 0.00 0 0 0.00 0 0 0.00 0 0",
|
||||||
|
engineNames: []string{"RCS", "BCS", "VCS"},
|
||||||
|
friendlyNames: []string{"Render/3D", "Blitter", "Video"},
|
||||||
|
powerIndex: 4,
|
||||||
|
preEngineCols: 8,
|
||||||
|
wantPowerGPU: 0.00,
|
||||||
|
wantEngines: map[string]float64{
|
||||||
|
"Render/3D": 0.00,
|
||||||
|
"Blitter": 0.00,
|
||||||
|
"Video": 0.00,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "data with no power index",
|
||||||
|
line: "373 373 224 45 1.50 4.13 2554 714 12.34 0 0 0.00 0 0 5.00 0 0",
|
||||||
|
engineNames: []string{"RCS", "BCS", "VCS"},
|
||||||
|
friendlyNames: []string{"Render/3D", "Blitter", "Video"},
|
||||||
|
powerIndex: -1,
|
||||||
|
preEngineCols: 8,
|
||||||
|
wantPowerGPU: 0.0, // no power parsed
|
||||||
|
wantEngines: map[string]float64{
|
||||||
|
"Render/3D": 12.34,
|
||||||
|
"Blitter": 0.00,
|
||||||
|
"Video": 5.00,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "data with insufficient columns",
|
||||||
|
line: "373 373 224 45 1.50", // too few columns
|
||||||
|
engineNames: []string{"RCS", "BCS", "VCS"},
|
||||||
|
friendlyNames: []string{"Render/3D", "Blitter", "Video"},
|
||||||
|
powerIndex: 4,
|
||||||
|
preEngineCols: 8,
|
||||||
|
wantPowerGPU: 0.0,
|
||||||
|
wantEngines: nil, // empty sample returned
|
||||||
|
wantErr: errNoValidData,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty line",
|
||||||
|
line: "",
|
||||||
|
engineNames: []string{"RCS"},
|
||||||
|
friendlyNames: []string{"Render/3D"},
|
||||||
|
powerIndex: 4,
|
||||||
|
preEngineCols: 8,
|
||||||
|
wantPowerGPU: 0.0,
|
||||||
|
wantEngines: nil,
|
||||||
|
wantErr: errNoValidData,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "data with invalid power value",
|
||||||
|
line: "373 373 224 45 N/A 4.13 2554 714 12.34 0 0 0.00 0 0 5.00 0 0",
|
||||||
|
engineNames: []string{"RCS", "BCS", "VCS"},
|
||||||
|
friendlyNames: []string{"Render/3D", "Blitter", "Video"},
|
||||||
|
powerIndex: 4,
|
||||||
|
preEngineCols: 8,
|
||||||
|
wantPowerGPU: 0.0, // N/A can't be parsed
|
||||||
|
wantEngines: map[string]float64{
|
||||||
|
"Render/3D": 12.34,
|
||||||
|
"Blitter": 0.00,
|
||||||
|
"Video": 5.00,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "data with invalid engine value",
|
||||||
|
line: "373 373 224 45 1.50 4.13 2554 714 N/A 0 0 0.00 0 0 5.00 0 0",
|
||||||
|
engineNames: []string{"RCS", "BCS", "VCS"},
|
||||||
|
friendlyNames: []string{"Render/3D", "Blitter", "Video"},
|
||||||
|
powerIndex: 4,
|
||||||
|
preEngineCols: 8,
|
||||||
|
wantPowerGPU: 1.50,
|
||||||
|
wantEngines: map[string]float64{
|
||||||
|
"Render/3D": 0.0, // N/A becomes 0
|
||||||
|
"Blitter": 0.00,
|
||||||
|
"Video": 5.00,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "data with no engines",
|
||||||
|
line: "373 373 224 45 1.50 4.13 2554 714",
|
||||||
|
engineNames: []string{},
|
||||||
|
friendlyNames: []string{},
|
||||||
|
powerIndex: 4,
|
||||||
|
preEngineCols: 8,
|
||||||
|
wantPowerGPU: 1.50,
|
||||||
|
wantEngines: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
gm := &GPUManager{}
|
||||||
|
sample, err := gm.parseIntelData(tt.line, tt.engineNames, tt.friendlyNames, tt.powerIndex, tt.preEngineCols)
|
||||||
|
assert.Equal(t, tt.wantErr, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.wantPowerGPU, sample.PowerGPU)
|
||||||
|
assert.Equal(t, tt.wantEngines, sample.Engines)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package agent
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -13,6 +14,69 @@ import (
|
|||||||
|
|
||||||
var netInterfaceDeltaTracker = deltatracker.NewDeltaTracker[string, uint64]()
|
var netInterfaceDeltaTracker = deltatracker.NewDeltaTracker[string, uint64]()
|
||||||
|
|
||||||
|
// NicConfig controls inclusion/exclusion of network interfaces via the NICS env var
|
||||||
|
//
|
||||||
|
// Behavior mirrors SensorConfig's matching logic:
|
||||||
|
// - Leading '-' means blacklist mode; otherwise whitelist mode
|
||||||
|
// - Supports '*' wildcards using path.Match
|
||||||
|
// - In whitelist mode with an empty list, no NICs are selected
|
||||||
|
// - In blacklist mode with an empty list, all NICs are selected
|
||||||
|
type NicConfig struct {
|
||||||
|
nics map[string]struct{}
|
||||||
|
isBlacklist bool
|
||||||
|
hasWildcards bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNicConfig(nicsEnvVal string) *NicConfig {
|
||||||
|
cfg := &NicConfig{
|
||||||
|
nics: make(map[string]struct{}),
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(nicsEnvVal, "-") {
|
||||||
|
cfg.isBlacklist = true
|
||||||
|
nicsEnvVal = nicsEnvVal[1:]
|
||||||
|
}
|
||||||
|
for nic := range strings.SplitSeq(nicsEnvVal, ",") {
|
||||||
|
nic = strings.TrimSpace(nic)
|
||||||
|
if nic != "" {
|
||||||
|
cfg.nics[nic] = struct{}{}
|
||||||
|
if strings.Contains(nic, "*") {
|
||||||
|
cfg.hasWildcards = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValidNic determines if a NIC should be included based on NicConfig rules
|
||||||
|
func isValidNic(nicName string, cfg *NicConfig) bool {
|
||||||
|
// Empty list behavior differs by mode: blacklist: allow all; whitelist: allow none
|
||||||
|
if len(cfg.nics) == 0 {
|
||||||
|
return cfg.isBlacklist
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exact match: return true if whitelist, false if blacklist
|
||||||
|
if _, exactMatch := cfg.nics[nicName]; exactMatch {
|
||||||
|
return !cfg.isBlacklist
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no wildcards, return true if blacklist, false if whitelist
|
||||||
|
if !cfg.hasWildcards {
|
||||||
|
return cfg.isBlacklist
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for wildcard patterns
|
||||||
|
for pattern := range cfg.nics {
|
||||||
|
if !strings.Contains(pattern, "*") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if match, _ := path.Match(pattern, nicName); match {
|
||||||
|
return !cfg.isBlacklist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg.isBlacklist
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Agent) updateNetworkStats(systemStats *system.Stats) {
|
func (a *Agent) updateNetworkStats(systemStats *system.Stats) {
|
||||||
// network stats
|
// network stats
|
||||||
if len(a.netInterfaces) == 0 {
|
if len(a.netInterfaces) == 0 {
|
||||||
@@ -89,14 +153,11 @@ func (a *Agent) initializeNetIoStats() {
|
|||||||
// reset valid network interfaces
|
// reset valid network interfaces
|
||||||
a.netInterfaces = make(map[string]struct{}, 0)
|
a.netInterfaces = make(map[string]struct{}, 0)
|
||||||
|
|
||||||
// map of network interface names passed in via NICS env var
|
// parse NICS env var for whitelist / blacklist
|
||||||
var nicsMap map[string]struct{}
|
nicsEnvVal, nicsEnvExists := GetEnv("NICS")
|
||||||
nics, nicsEnvExists := GetEnv("NICS")
|
var nicCfg *NicConfig
|
||||||
if nicsEnvExists {
|
if nicsEnvExists {
|
||||||
nicsMap = make(map[string]struct{}, 0)
|
nicCfg = newNicConfig(nicsEnvVal)
|
||||||
for nic := range strings.SplitSeq(nics, ",") {
|
|
||||||
nicsMap[nic] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset network I/O stats
|
// reset network I/O stats
|
||||||
@@ -107,17 +168,11 @@ func (a *Agent) initializeNetIoStats() {
|
|||||||
if netIO, err := psutilNet.IOCounters(true); err == nil {
|
if netIO, err := psutilNet.IOCounters(true); err == nil {
|
||||||
a.netIoStats.Time = time.Now()
|
a.netIoStats.Time = time.Now()
|
||||||
for _, v := range netIO {
|
for _, v := range netIO {
|
||||||
switch {
|
if nicsEnvExists && !isValidNic(v.Name, nicCfg) {
|
||||||
// skip if nics exists and the interface is not in the list
|
continue
|
||||||
case nicsEnvExists:
|
}
|
||||||
if _, nameInNics := nicsMap[v.Name]; !nameInNics {
|
if a.skipNetworkInterface(v) {
|
||||||
continue
|
continue
|
||||||
}
|
|
||||||
// otherwise run the interface name through the skipNetworkInterface function
|
|
||||||
default:
|
|
||||||
if a.skipNetworkInterface(v) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
slog.Info("Detected network interface", "name", v.Name, "sent", v.BytesSent, "recv", v.BytesRecv)
|
slog.Info("Detected network interface", "name", v.Name, "sent", v.BytesSent, "recv", v.BytesRecv)
|
||||||
a.netIoStats.BytesSent += v.BytesSent
|
a.netIoStats.BytesSent += v.BytesSent
|
||||||
@@ -135,6 +190,7 @@ func (a *Agent) skipNetworkInterface(v psutilNet.IOCountersStat) bool {
|
|||||||
strings.HasPrefix(v.Name, "br-"),
|
strings.HasPrefix(v.Name, "br-"),
|
||||||
strings.HasPrefix(v.Name, "veth"),
|
strings.HasPrefix(v.Name, "veth"),
|
||||||
strings.HasPrefix(v.Name, "bond"),
|
strings.HasPrefix(v.Name, "bond"),
|
||||||
|
strings.HasPrefix(v.Name, "cali"),
|
||||||
v.BytesRecv == 0,
|
v.BytesRecv == 0,
|
||||||
v.BytesSent == 0:
|
v.BytesSent == 0:
|
||||||
return true
|
return true
|
||||||
|
|||||||
259
agent/network_test.go
Normal file
259
agent/network_test.go
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
//go:build testing
|
||||||
|
|
||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsValidNic(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
nicName string
|
||||||
|
config *NicConfig
|
||||||
|
expectedValid bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Whitelist - NIC in list",
|
||||||
|
nicName: "eth0",
|
||||||
|
config: &NicConfig{
|
||||||
|
nics: map[string]struct{}{"eth0": {}},
|
||||||
|
isBlacklist: false,
|
||||||
|
},
|
||||||
|
expectedValid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Whitelist - NIC not in list",
|
||||||
|
nicName: "wlan0",
|
||||||
|
config: &NicConfig{
|
||||||
|
nics: map[string]struct{}{"eth0": {}},
|
||||||
|
isBlacklist: false,
|
||||||
|
},
|
||||||
|
expectedValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Blacklist - NIC in list",
|
||||||
|
nicName: "eth0",
|
||||||
|
config: &NicConfig{
|
||||||
|
nics: map[string]struct{}{"eth0": {}},
|
||||||
|
isBlacklist: true,
|
||||||
|
},
|
||||||
|
expectedValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Blacklist - NIC not in list",
|
||||||
|
nicName: "wlan0",
|
||||||
|
config: &NicConfig{
|
||||||
|
nics: map[string]struct{}{"eth0": {}},
|
||||||
|
isBlacklist: true,
|
||||||
|
},
|
||||||
|
expectedValid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Whitelist with wildcard - matching pattern",
|
||||||
|
nicName: "eth1",
|
||||||
|
config: &NicConfig{
|
||||||
|
nics: map[string]struct{}{"eth*": {}},
|
||||||
|
isBlacklist: false,
|
||||||
|
hasWildcards: true,
|
||||||
|
},
|
||||||
|
expectedValid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Whitelist with wildcard - non-matching pattern",
|
||||||
|
nicName: "wlan0",
|
||||||
|
config: &NicConfig{
|
||||||
|
nics: map[string]struct{}{"eth*": {}},
|
||||||
|
isBlacklist: false,
|
||||||
|
hasWildcards: true,
|
||||||
|
},
|
||||||
|
expectedValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Blacklist with wildcard - matching pattern",
|
||||||
|
nicName: "eth1",
|
||||||
|
config: &NicConfig{
|
||||||
|
nics: map[string]struct{}{"eth*": {}},
|
||||||
|
isBlacklist: true,
|
||||||
|
hasWildcards: true,
|
||||||
|
},
|
||||||
|
expectedValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Blacklist with wildcard - non-matching pattern",
|
||||||
|
nicName: "wlan0",
|
||||||
|
config: &NicConfig{
|
||||||
|
nics: map[string]struct{}{"eth*": {}},
|
||||||
|
isBlacklist: true,
|
||||||
|
hasWildcards: true,
|
||||||
|
},
|
||||||
|
expectedValid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty whitelist config - no NICs allowed",
|
||||||
|
nicName: "eth0",
|
||||||
|
config: &NicConfig{
|
||||||
|
nics: map[string]struct{}{},
|
||||||
|
isBlacklist: false,
|
||||||
|
},
|
||||||
|
expectedValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty blacklist config - all NICs allowed",
|
||||||
|
nicName: "eth0",
|
||||||
|
config: &NicConfig{
|
||||||
|
nics: map[string]struct{}{},
|
||||||
|
isBlacklist: true,
|
||||||
|
},
|
||||||
|
expectedValid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Multiple patterns - exact match",
|
||||||
|
nicName: "eth0",
|
||||||
|
config: &NicConfig{
|
||||||
|
nics: map[string]struct{}{"eth0": {}, "wlan*": {}},
|
||||||
|
isBlacklist: false,
|
||||||
|
},
|
||||||
|
expectedValid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Multiple patterns - wildcard match",
|
||||||
|
nicName: "wlan1",
|
||||||
|
config: &NicConfig{
|
||||||
|
nics: map[string]struct{}{"eth0": {}, "wlan*": {}},
|
||||||
|
isBlacklist: false,
|
||||||
|
hasWildcards: true,
|
||||||
|
},
|
||||||
|
expectedValid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Multiple patterns - no match",
|
||||||
|
nicName: "bond0",
|
||||||
|
config: &NicConfig{
|
||||||
|
nics: map[string]struct{}{"eth0": {}, "wlan*": {}},
|
||||||
|
isBlacklist: false,
|
||||||
|
hasWildcards: true,
|
||||||
|
},
|
||||||
|
expectedValid: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := isValidNic(tt.nicName, tt.config)
|
||||||
|
assert.Equal(t, tt.expectedValid, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewNicConfig(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
nicsEnvVal string
|
||||||
|
expectedCfg *NicConfig
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Empty string",
|
||||||
|
nicsEnvVal: "",
|
||||||
|
expectedCfg: &NicConfig{
|
||||||
|
nics: map[string]struct{}{},
|
||||||
|
isBlacklist: false,
|
||||||
|
hasWildcards: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Single NIC whitelist",
|
||||||
|
nicsEnvVal: "eth0",
|
||||||
|
expectedCfg: &NicConfig{
|
||||||
|
nics: map[string]struct{}{"eth0": {}},
|
||||||
|
isBlacklist: false,
|
||||||
|
hasWildcards: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Multiple NICs whitelist",
|
||||||
|
nicsEnvVal: "eth0,wlan0",
|
||||||
|
expectedCfg: &NicConfig{
|
||||||
|
nics: map[string]struct{}{"eth0": {}, "wlan0": {}},
|
||||||
|
isBlacklist: false,
|
||||||
|
hasWildcards: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Blacklist mode",
|
||||||
|
nicsEnvVal: "-eth0,wlan0",
|
||||||
|
expectedCfg: &NicConfig{
|
||||||
|
nics: map[string]struct{}{"eth0": {}, "wlan0": {}},
|
||||||
|
isBlacklist: true,
|
||||||
|
hasWildcards: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "With wildcards",
|
||||||
|
nicsEnvVal: "eth*,wlan0",
|
||||||
|
expectedCfg: &NicConfig{
|
||||||
|
nics: map[string]struct{}{"eth*": {}, "wlan0": {}},
|
||||||
|
isBlacklist: false,
|
||||||
|
hasWildcards: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Blacklist with wildcards",
|
||||||
|
nicsEnvVal: "-eth*,wlan0",
|
||||||
|
expectedCfg: &NicConfig{
|
||||||
|
nics: map[string]struct{}{"eth*": {}, "wlan0": {}},
|
||||||
|
isBlacklist: true,
|
||||||
|
hasWildcards: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "With whitespace",
|
||||||
|
nicsEnvVal: "eth0, wlan0 , eth1",
|
||||||
|
expectedCfg: &NicConfig{
|
||||||
|
nics: map[string]struct{}{"eth0": {}, "wlan0": {}, "eth1": {}},
|
||||||
|
isBlacklist: false,
|
||||||
|
hasWildcards: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Only wildcards",
|
||||||
|
nicsEnvVal: "eth*,wlan*",
|
||||||
|
expectedCfg: &NicConfig{
|
||||||
|
nics: map[string]struct{}{"eth*": {}, "wlan*": {}},
|
||||||
|
isBlacklist: false,
|
||||||
|
hasWildcards: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Leading dash only",
|
||||||
|
nicsEnvVal: "-",
|
||||||
|
expectedCfg: &NicConfig{
|
||||||
|
nics: map[string]struct{}{},
|
||||||
|
isBlacklist: true,
|
||||||
|
hasWildcards: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Mixed exact and wildcard",
|
||||||
|
nicsEnvVal: "eth0,br-*",
|
||||||
|
expectedCfg: &NicConfig{
|
||||||
|
nics: map[string]struct{}{"eth0": {}, "br-*": {}},
|
||||||
|
isBlacklist: false,
|
||||||
|
hasWildcards: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cfg := newNicConfig(tt.nicsEnvVal)
|
||||||
|
require.NotNil(t, cfg)
|
||||||
|
assert.Equal(t, tt.expectedCfg.isBlacklist, cfg.isBlacklist)
|
||||||
|
assert.Equal(t, tt.expectedCfg.hasWildcards, cfg.hasWildcards)
|
||||||
|
assert.Equal(t, tt.expectedCfg.nics, cfg.nics)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -100,14 +100,22 @@ func (a *Agent) getSystemStats() system.Stats {
|
|||||||
systemStats.Swap = bytesToGigabytes(v.SwapTotal)
|
systemStats.Swap = bytesToGigabytes(v.SwapTotal)
|
||||||
systemStats.SwapUsed = bytesToGigabytes(v.SwapTotal - v.SwapFree - v.SwapCached)
|
systemStats.SwapUsed = bytesToGigabytes(v.SwapTotal - v.SwapFree - v.SwapCached)
|
||||||
// cache + buffers value for default mem calculation
|
// cache + buffers value for default mem calculation
|
||||||
cacheBuff := v.Total - v.Free - v.Used
|
// note: gopsutil automatically adds SReclaimable to v.Cached
|
||||||
// htop memory calculation overrides
|
cacheBuff := v.Cached + v.Buffers - v.Shared
|
||||||
|
if cacheBuff <= 0 {
|
||||||
|
cacheBuff = max(v.Total-v.Free-v.Used, 0)
|
||||||
|
}
|
||||||
|
// htop memory calculation overrides (likely outdated as of mid 2025)
|
||||||
if a.memCalc == "htop" {
|
if a.memCalc == "htop" {
|
||||||
// note: gopsutil automatically adds SReclaimable to v.Cached
|
// cacheBuff = v.Cached + v.Buffers - v.Shared
|
||||||
cacheBuff = v.Cached + v.Buffers - v.Shared
|
|
||||||
v.Used = v.Total - (v.Free + cacheBuff)
|
v.Used = v.Total - (v.Free + cacheBuff)
|
||||||
v.UsedPercent = float64(v.Used) / float64(v.Total) * 100.0
|
v.UsedPercent = float64(v.Used) / float64(v.Total) * 100.0
|
||||||
}
|
}
|
||||||
|
// if a.memCalc == "legacy" {
|
||||||
|
// v.Used = v.Total - v.Free - v.Buffers - v.Cached
|
||||||
|
// cacheBuff = v.Total - v.Free - v.Used
|
||||||
|
// v.UsedPercent = float64(v.Used) / float64(v.Total) * 100.0
|
||||||
|
// }
|
||||||
// subtract ZFS ARC size from used memory and add as its own category
|
// subtract ZFS ARC size from used memory and add as its own category
|
||||||
if a.zfs {
|
if a.zfs {
|
||||||
if arcSize, _ := getARCSize(); arcSize > 0 && arcSize < v.Used {
|
if arcSize, _ := getARCSize(); arcSize > 0 && arcSize < v.Used {
|
||||||
|
|||||||
@@ -30,11 +30,11 @@ func (s *systemdRestarter) Restart() error {
|
|||||||
type openRCRestarter struct{ cmd string }
|
type openRCRestarter struct{ cmd string }
|
||||||
|
|
||||||
func (o *openRCRestarter) Restart() error {
|
func (o *openRCRestarter) Restart() error {
|
||||||
if err := exec.Command(o.cmd, "status", "beszel-agent").Run(); err != nil {
|
if err := exec.Command(o.cmd, "beszel-agent", "status").Run(); err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
ghupdate.ColorPrint(ghupdate.ColorYellow, "Restarting beszel-agent via OpenRC…")
|
ghupdate.ColorPrint(ghupdate.ColorYellow, "Restarting beszel-agent via OpenRC…")
|
||||||
return exec.Command(o.cmd, "restart", "beszel-agent").Run()
|
return exec.Command(o.cmd, "beszel-agent", "restart").Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
type openWRTRestarter struct{ cmd string }
|
type openWRTRestarter struct{ cmd string }
|
||||||
|
|||||||
@@ -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.12.10"
|
Version = "0.12.12"
|
||||||
// AppName is the name of the application.
|
// AppName is the name of the application.
|
||||||
AppName = "beszel"
|
AppName = "beszel"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ ARG TARGETOS TARGETARCH
|
|||||||
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./internal/cmd/agent
|
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./internal/cmd/agent
|
||||||
|
|
||||||
# --------------------------
|
# --------------------------
|
||||||
# Final image: default scratch-based agent
|
# Final image
|
||||||
# Note: must cap_add: [CAP_PERFMON] in docker-compose.yml
|
# Note: must cap_add: [CAP_PERFMON] and mount /dev/dri/ as volume
|
||||||
# --------------------------
|
# --------------------------
|
||||||
FROM alpine:edge
|
FROM alpine:edge
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ type GPUData struct {
|
|||||||
Power float64 `json:"p,omitempty" cbor:"4,keyasint,omitempty"`
|
Power float64 `json:"p,omitempty" cbor:"4,keyasint,omitempty"`
|
||||||
Count float64 `json:"-"`
|
Count float64 `json:"-"`
|
||||||
Engines map[string]float64 `json:"e,omitempty" cbor:"5,keyasint,omitempty"`
|
Engines map[string]float64 `json:"e,omitempty" cbor:"5,keyasint,omitempty"`
|
||||||
|
PowerPkg float64 `json:"pp,omitempty" cbor:"6,keyasint,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FsStats struct {
|
type FsStats struct {
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ func Update(cmd *cobra.Command, _ []string) {
|
|||||||
// Check if china-mirrors flag is set
|
// Check if china-mirrors flag is set
|
||||||
useMirror, _ := cmd.Flags().GetBool("china-mirrors")
|
useMirror, _ := cmd.Flags().GetBool("china-mirrors")
|
||||||
|
|
||||||
|
// Get the executable path before update
|
||||||
|
exePath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
updated, err := ghupdate.Update(ghupdate.Config{
|
updated, err := ghupdate.Update(ghupdate.Config{
|
||||||
ArchiveExecutable: "beszel",
|
ArchiveExecutable: "beszel",
|
||||||
DataDir: dataDir,
|
DataDir: dataDir,
|
||||||
@@ -35,11 +41,8 @@ func Update(cmd *cobra.Command, _ []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// make sure the file is executable
|
// make sure the file is executable
|
||||||
exePath, err := os.Executable()
|
if err := os.Chmod(exePath, 0755); err != nil {
|
||||||
if err == nil {
|
fmt.Printf("Warning: failed to set executable permissions: %v\n", err)
|
||||||
if err := os.Chmod(exePath, 0755); err != nil {
|
|
||||||
fmt.Printf("Warning: failed to set executable permissions: %v\n", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to restart the service if it's running
|
// Try to restart the service if it's running
|
||||||
|
|||||||
50
internal/migrations/1758738789_fix_cached_mem.go
Normal file
50
internal/migrations/1758738789_fix_cached_mem.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
m "github.com/pocketbase/pocketbase/migrations"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This can be deleted after Nov 2025 or so
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
m.Register(func(app core.App) error {
|
||||||
|
app.RunInTransaction(func(txApp core.App) error {
|
||||||
|
var systemIds []string
|
||||||
|
txApp.DB().NewQuery("SELECT id FROM systems").Column(&systemIds)
|
||||||
|
|
||||||
|
for _, systemId := range systemIds {
|
||||||
|
var statRecordIds []string
|
||||||
|
txApp.DB().NewQuery("SELECT id FROM system_stats WHERE system = {:system} AND created > {:created}").Bind(map[string]any{"system": systemId, "created": "2025-09-21"}).Column(&statRecordIds)
|
||||||
|
|
||||||
|
for _, statRecordId := range statRecordIds {
|
||||||
|
statRecord, err := txApp.FindRecordById("system_stats", statRecordId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var systemStats system.Stats
|
||||||
|
err = statRecord.UnmarshalJSONField("stats", &systemStats)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// if mem buff cache is less than total mem, we don't need to fix it
|
||||||
|
if systemStats.MemBuffCache < systemStats.Mem {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
systemStats.MemBuffCache = 0
|
||||||
|
statRecord.Set("stats", systemStats)
|
||||||
|
err = txApp.SaveNoValidate(statRecord)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}, func(app core.App) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
4
internal/site/package-lock.json
generated
4
internal/site/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "beszel",
|
"name": "beszel",
|
||||||
"version": "0.12.10",
|
"version": "0.12.12",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "beszel",
|
"name": "beszel",
|
||||||
"version": "0.12.10",
|
"version": "0.12.12",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@henrygd/queue": "^1.0.7",
|
"@henrygd/queue": "^1.0.7",
|
||||||
"@henrygd/semaphore": "^0.0.2",
|
"@henrygd/semaphore": "^0.0.2",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "beszel",
|
"name": "beszel",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.12.10",
|
"version": "0.12.12",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host",
|
"dev": "vite --host",
|
||||||
|
|||||||
@@ -9,46 +9,58 @@ import {
|
|||||||
xAxis,
|
xAxis,
|
||||||
} from "@/components/ui/chart"
|
} from "@/components/ui/chart"
|
||||||
import { chartMargin, cn, decimalString, formatShortDate, toFixedFloat } from "@/lib/utils"
|
import { chartMargin, cn, decimalString, formatShortDate, toFixedFloat } from "@/lib/utils"
|
||||||
import type { ChartData } from "@/types"
|
import type { ChartData, GPUData } from "@/types"
|
||||||
import { useYAxisWidth } from "./hooks"
|
import { useYAxisWidth } from "./hooks"
|
||||||
|
import type { DataPoint } from "./line-chart"
|
||||||
|
|
||||||
export default memo(function GpuPowerChart({ chartData }: { chartData: ChartData }) {
|
export default memo(function GpuPowerChart({ chartData }: { chartData: ChartData }) {
|
||||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||||
|
const packageKey = " package"
|
||||||
|
|
||||||
|
const { gpuData, dataPoints } = useMemo(() => {
|
||||||
|
const dataPoints = [] as DataPoint[]
|
||||||
|
const gpuData = [] as Record<string, GPUData | string>[]
|
||||||
|
const addedKeys = new Map<string, number>()
|
||||||
|
|
||||||
|
const addKey = (key: string, value: number) => {
|
||||||
|
addedKeys.set(key, (addedKeys.get(key) ?? 0) + value)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const stats of chartData.systemStats) {
|
||||||
|
const gpus = stats.stats?.g ?? {}
|
||||||
|
const data = { created: stats.created } as Record<string, GPUData | string>
|
||||||
|
for (const id in gpus) {
|
||||||
|
const gpu = gpus[id] as GPUData
|
||||||
|
data[gpu.n] = gpu
|
||||||
|
addKey(gpu.n, gpu.p ?? 0)
|
||||||
|
if (gpu.pp) {
|
||||||
|
data[`${gpu.n}${packageKey}`] = gpu
|
||||||
|
addKey(`${gpu.n}${packageKey}`, gpu.pp ?? 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gpuData.push(data)
|
||||||
|
}
|
||||||
|
const sortedKeys = Array.from(addedKeys.entries())
|
||||||
|
.sort(([, a], [, b]) => b - a)
|
||||||
|
.map(([key]) => key)
|
||||||
|
|
||||||
|
for (let i = 0; i < sortedKeys.length; i++) {
|
||||||
|
const id = sortedKeys[i]
|
||||||
|
dataPoints.push({
|
||||||
|
label: id,
|
||||||
|
dataKey: (gpuData: Record<string, GPUData>) => {
|
||||||
|
return id.endsWith(packageKey) ? (gpuData[id]?.pp ?? 0) : (gpuData[id]?.p ?? 0)
|
||||||
|
},
|
||||||
|
color: `hsl(${226 + (((i * 360) / addedKeys.size) % 360)}, 65%, 52%)`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return { gpuData, dataPoints }
|
||||||
|
}, [chartData])
|
||||||
|
|
||||||
if (chartData.systemStats.length === 0) {
|
if (chartData.systemStats.length === 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Format temperature data for chart and assign colors */
|
|
||||||
const newChartData = useMemo(() => {
|
|
||||||
const newChartData = { data: [], colors: {} } as {
|
|
||||||
data: Record<string, number | string>[]
|
|
||||||
colors: Record<string, string>
|
|
||||||
}
|
|
||||||
const powerSums = {} as Record<string, number>
|
|
||||||
for (const data of chartData.systemStats) {
|
|
||||||
const newData = { created: data.created } as Record<string, number | string>
|
|
||||||
|
|
||||||
for (const gpu of Object.values(data.stats?.g ?? {})) {
|
|
||||||
if (gpu.p) {
|
|
||||||
const name = gpu.n
|
|
||||||
newData[name] = gpu.p
|
|
||||||
powerSums[name] = (powerSums[name] ?? 0) + newData[name]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newChartData.data.push(newData)
|
|
||||||
}
|
|
||||||
const keys = Object.keys(powerSums).sort((a, b) => powerSums[b] - powerSums[a])
|
|
||||||
for (const key of keys) {
|
|
||||||
newChartData.colors[key] = `hsl(${((keys.indexOf(key) * 360) / keys.length) % 360}, 60%, 55%)`
|
|
||||||
}
|
|
||||||
return newChartData
|
|
||||||
}, [chartData])
|
|
||||||
|
|
||||||
const colors = Object.keys(newChartData.colors)
|
|
||||||
|
|
||||||
// console.log('rendered at', new Date())
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ChartContainer
|
<ChartContainer
|
||||||
@@ -56,7 +68,7 @@ export default memo(function GpuPowerChart({ chartData }: { chartData: ChartData
|
|||||||
"opacity-100": yAxisWidth,
|
"opacity-100": yAxisWidth,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<LineChart accessibilityLayer data={newChartData.data} margin={chartMargin}>
|
<LineChart accessibilityLayer data={gpuData} margin={chartMargin}>
|
||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
<YAxis
|
<YAxis
|
||||||
direction="ltr"
|
direction="ltr"
|
||||||
@@ -85,19 +97,19 @@ export default memo(function GpuPowerChart({ chartData }: { chartData: ChartData
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{colors.map((key) => (
|
{dataPoints.map((dataPoint) => (
|
||||||
<Line
|
<Line
|
||||||
key={key}
|
key={dataPoint.label}
|
||||||
dataKey={key}
|
dataKey={dataPoint.dataKey}
|
||||||
name={key}
|
name={dataPoint.label}
|
||||||
type="monotoneX"
|
type="monotoneX"
|
||||||
dot={false}
|
dot={false}
|
||||||
strokeWidth={1.5}
|
strokeWidth={1.5}
|
||||||
stroke={newChartData.colors[key]}
|
stroke={dataPoint.color as string}
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{colors.length > 1 && <ChartLegend content={<ChartLegendContent />} />}
|
{dataPoints.length > 1 && <ChartLegend content={<ChartLegendContent />} />}
|
||||||
</LineChart>
|
</LineChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,7 +24,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, Os, SystemStatus, Unit } from "@/lib/enums"
|
import { ChartType, ConnectionType, connectionTypeLabels, Os, SystemStatus, Unit } from "@/lib/enums"
|
||||||
import { batteryStateTranslations } from "@/lib/i18n"
|
import { batteryStateTranslations } from "@/lib/i18n"
|
||||||
import {
|
import {
|
||||||
$allSystemsByName,
|
$allSystemsByName,
|
||||||
@@ -398,7 +398,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
|
|||||||
const dataEmpty = !chartLoading && chartData.systemStats.length === 0
|
const dataEmpty = !chartLoading && chartData.systemStats.length === 0
|
||||||
const lastGpuVals = Object.values(systemStats.at(-1)?.stats.g ?? {})
|
const lastGpuVals = Object.values(systemStats.at(-1)?.stats.g ?? {})
|
||||||
const hasGpuData = lastGpuVals.length > 0
|
const hasGpuData = lastGpuVals.length > 0
|
||||||
const hasGpuPowerData = lastGpuVals.some((gpu) => gpu.p !== 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
|
let translatedStatus: string = system.status
|
||||||
@@ -442,15 +442,14 @@ export default memo(function SystemDetail({ name }: { name: string }) {
|
|||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
{system.info.ct && (
|
{system.info.ct && (
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{system.info.ct === ConnectionType.WebSocket ? (
|
<div className="flex gap-1 items-center">
|
||||||
<div className="flex gap-1 items-center">
|
{system.info.ct === ConnectionType.WebSocket ? (
|
||||||
<WebSocketIcon className="size-4" /> WebSocket
|
<WebSocketIcon className="size-4" />
|
||||||
</div>
|
) : (
|
||||||
) : (
|
<ChevronRightSquareIcon className="size-4" strokeWidth={2} />
|
||||||
<div className="flex gap-1 items-center">
|
)}
|
||||||
<ChevronRightSquareIcon className="size-4" strokeWidth={2} /> SSH
|
{connectionTypeLabels[system.info.ct as ConnectionType]}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
)}
|
)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -938,24 +937,22 @@ function GpuEnginesChart({ chartData }: { chartData: ChartData }) {
|
|||||||
function FilterBar({ store = $containerFilter }: { store?: typeof $containerFilter }) {
|
function FilterBar({ store = $containerFilter }: { store?: typeof $containerFilter }) {
|
||||||
const containerFilter = useStore(store)
|
const containerFilter = useStore(store)
|
||||||
const { t } = useLingui()
|
const { t } = useLingui()
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
|
||||||
|
|
||||||
const debouncedStoreSet = useMemo(() => debounce((value: string) => store.set(value), 150), [store])
|
const debouncedStoreSet = useMemo(() => debounce((value: string) => store.set(value), 80), [store])
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
(e: React.ChangeEvent<HTMLInputElement>) => debouncedStoreSet(e.target.value),
|
||||||
const value = e.target.value
|
|
||||||
if (inputRef.current) {
|
|
||||||
inputRef.current.value = value
|
|
||||||
}
|
|
||||||
debouncedStoreSet(value)
|
|
||||||
},
|
|
||||||
[debouncedStoreSet]
|
[debouncedStoreSet]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Input placeholder={t`Filter...`} className="ps-4 pe-8 w-full sm:w-44" onChange={handleChange} ref={inputRef} />
|
<Input
|
||||||
|
placeholder={t`Filter...`}
|
||||||
|
className="ps-4 pe-8 w-full sm:w-44"
|
||||||
|
onChange={handleChange}
|
||||||
|
value={containerFilter}
|
||||||
|
/>
|
||||||
{containerFilter && (
|
{containerFilter && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -963,12 +960,7 @@ function FilterBar({ store = $containerFilter }: { store?: typeof $containerFilt
|
|||||||
size="icon"
|
size="icon"
|
||||||
aria-label="Clear"
|
aria-label="Clear"
|
||||||
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100"
|
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100"
|
||||||
onClick={() => {
|
onClick={() => store.set("")}
|
||||||
if (inputRef.current) {
|
|
||||||
inputRef.current.value = ""
|
|
||||||
}
|
|
||||||
store.set("")
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<XIcon className="h-4 w-4" />
|
<XIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import ChartTimeSelect from "@/components/charts/chart-time-select"
|
|||||||
import { useNetworkInterfaces } from "@/components/charts/hooks"
|
import { useNetworkInterfaces } from "@/components/charts/hooks"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
|
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
|
||||||
|
import { DialogTitle } from "@/components/ui/dialog"
|
||||||
import { $userSettings } from "@/lib/stores"
|
import { $userSettings } from "@/lib/stores"
|
||||||
import { decimalString, formatBytes, toFixedFloat } from "@/lib/utils"
|
import { decimalString, formatBytes, toFixedFloat } from "@/lib/utils"
|
||||||
import type { ChartData } from "@/types"
|
import type { ChartData } from "@/types"
|
||||||
@@ -26,7 +27,7 @@ export default memo(function NetworkSheet({
|
|||||||
const [netInterfacesOpen, setNetInterfacesOpen] = useState(false)
|
const [netInterfacesOpen, setNetInterfacesOpen] = useState(false)
|
||||||
const userSettings = useStore($userSettings)
|
const userSettings = useStore($userSettings)
|
||||||
const netInterfaces = useNetworkInterfaces(chartData.systemStats.at(-1)?.stats?.ni ?? {})
|
const netInterfaces = useNetworkInterfaces(chartData.systemStats.at(-1)?.stats?.ni ?? {})
|
||||||
const showNetLegend = netInterfaces.length > 0
|
const showNetLegend = netInterfaces.length > 0 && netInterfaces.length < 15
|
||||||
const hasOpened = useRef(false)
|
const hasOpened = useRef(false)
|
||||||
|
|
||||||
if (netInterfacesOpen && !hasOpened.current) {
|
if (netInterfacesOpen && !hasOpened.current) {
|
||||||
@@ -39,9 +40,10 @@ export default memo(function NetworkSheet({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={netInterfacesOpen} onOpenChange={setNetInterfacesOpen}>
|
<Sheet open={netInterfacesOpen} onOpenChange={setNetInterfacesOpen}>
|
||||||
|
<DialogTitle className="sr-only">{t`Network traffic of public interfaces`}</DialogTitle>
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t`View more`}
|
title={t`View more`}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="shrink-0 max-sm:absolute max-sm:top-3 max-sm:end-3"
|
className="shrink-0 max-sm:absolute max-sm:top-3 max-sm:end-3"
|
||||||
@@ -50,7 +52,7 @@ export default memo(function NetworkSheet({
|
|||||||
</Button>
|
</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
{hasOpened.current && (
|
{hasOpened.current && (
|
||||||
<SheetContent className="overflow-auto w-200 !max-w-full p-4 sm:p-6">
|
<SheetContent aria-describedby={undefined} className="overflow-auto w-200 !max-w-full p-4 sm:p-6">
|
||||||
<ChartTimeSelect className="w-[calc(100%-2em)]" />
|
<ChartTimeSelect className="w-[calc(100%-2em)]" />
|
||||||
<ChartCard
|
<ChartCard
|
||||||
empty={dataEmpty}
|
empty={dataEmpty}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { memo, useMemo, useRef, useState } from "react"
|
import { memo, useMemo, useRef, useState } from "react"
|
||||||
import { isReadOnlyUser, pb } from "@/lib/api"
|
import { isReadOnlyUser, pb } from "@/lib/api"
|
||||||
import { ConnectionType, MeterState, SystemStatus } from "@/lib/enums"
|
import { ConnectionType, connectionTypeLabels, MeterState, SystemStatus } from "@/lib/enums"
|
||||||
import { $longestSystemNameLen, $userSettings } from "@/lib/stores"
|
import { $longestSystemNameLen, $userSettings } from "@/lib/stores"
|
||||||
import {
|
import {
|
||||||
cn,
|
cn,
|
||||||
@@ -278,12 +278,25 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
|
|||||||
"text-red-500": system.status !== SystemStatus.Up,
|
"text-red-500": system.status !== SystemStatus.Up,
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex gap-1.5 items-center md:pe-5 tabular-nums", viewMode === "table" && "ps-0.5")}>
|
<Link
|
||||||
{system.info.ct === ConnectionType.WebSocket && <WebSocketIcon className={cn("size-3", color)} />}
|
href={getPagePath($router, "system", { name: system.name })}
|
||||||
{system.info.ct === ConnectionType.SSH && <ChevronRightSquareIcon className={cn("size-3", color)} />}
|
className={cn(
|
||||||
|
"flex gap-1.5 items-center md:pe-5 tabular-nums relative z-10",
|
||||||
|
viewMode === "table" && "ps-0.5"
|
||||||
|
)}
|
||||||
|
tabIndex={-1}
|
||||||
|
title={connectionTypeLabels[system.info.ct as ConnectionType]}
|
||||||
|
role="none"
|
||||||
|
>
|
||||||
|
{system.info.ct === ConnectionType.WebSocket && (
|
||||||
|
<WebSocketIcon className={cn("size-3 pointer-events-none", color)} />
|
||||||
|
)}
|
||||||
|
{system.info.ct === ConnectionType.SSH && (
|
||||||
|
<ChevronRightSquareIcon className={cn("size-3 pointer-events-none", color)} />
|
||||||
|
)}
|
||||||
{!system.info.ct && <IndicatorDot system={system} className={cn(color, "bg-current mx-0.5")} />}
|
{!system.info.ct && <IndicatorDot system={system} className={cn(color, "bg-current mx-0.5")} />}
|
||||||
<span className="truncate max-w-14">{info.getValue() as string}</span>
|
<span className="truncate max-w-14">{info.getValue() as string}</span>
|
||||||
</div>
|
</Link>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ const AllSystemsTable = memo(
|
|||||||
function SystemsTableHead({ table }: { table: TableType<SystemRecord> }) {
|
function SystemsTableHead({ table }: { table: TableType<SystemRecord> }) {
|
||||||
const { t } = useLingui()
|
const { t } = useLingui()
|
||||||
return (
|
return (
|
||||||
<TableHeader className="sticky top-0 z-20 w-full border-b-2">
|
<TableHeader className="sticky top-0 z-50 w-full border-b-2">
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<tr key={headerGroup.id}>
|
<tr key={headerGroup.id}>
|
||||||
{headerGroup.headers.map((header) => {
|
{headerGroup.headers.map((header) => {
|
||||||
|
|||||||
@@ -59,3 +59,5 @@ export enum ConnectionType {
|
|||||||
SSH = 1,
|
SSH = 1,
|
||||||
WebSocket,
|
WebSocket,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const connectionTypeLabels = ["", "SSH", "WebSocket"] as const
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ msgstr "حركة مرور الشبكة لحاويات الدوكر"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr "حركة مرور الشبكة للواجهات العامة"
|
msgstr "حركة مرور الشبكة للواجهات العامة"
|
||||||
|
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ msgstr "Мрежов трафик на docker контейнери"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr "Мрежов трафик на публични интерфейси"
|
msgstr "Мрежов трафик на публични интерфейси"
|
||||||
|
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ msgstr "Síťový provoz kontejnerů docker"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr "Síťový provoz veřejných rozhraní"
|
msgstr "Síťový provoz veřejných rozhraní"
|
||||||
|
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ msgstr "Netværkstrafik af dockercontainere"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr "Netværkstrafik af offentlige grænseflader"
|
msgstr "Netværkstrafik af offentlige grænseflader"
|
||||||
|
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ msgstr "Netzwerkverkehr der Docker-Container"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr "Netzwerkverkehr der öffentlichen Schnittstellen"
|
msgstr "Netzwerkverkehr der öffentlichen Schnittstellen"
|
||||||
|
|
||||||
|
|||||||
@@ -709,6 +709,7 @@ msgstr "Network traffic of docker containers"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr "Network traffic of public interfaces"
|
msgstr "Network traffic of public interfaces"
|
||||||
|
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ msgstr "Tráfico de red de los contenedores de Docker"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr "Tráfico de red de interfaces públicas"
|
msgstr "Tráfico de red de interfaces públicas"
|
||||||
|
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ msgstr "ترافیک شبکه کانتینرهای داکر"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr "ترافیک شبکه رابطهای عمومی"
|
msgstr "ترافیک شبکه رابطهای عمومی"
|
||||||
|
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ msgstr "Trafic réseau des conteneurs Docker"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr "Trafic réseau des interfaces publiques"
|
msgstr "Trafic réseau des interfaces publiques"
|
||||||
|
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ msgstr ""
|
|||||||
"Language: hr\n"
|
"Language: hr\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2025-08-28 23:21\n"
|
"PO-Revision-Date: 2025-09-23 12:43\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Croatian\n"
|
"Language-Team: Croatian\n"
|
||||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||||
"X-Crowdin-Project: beszel\n"
|
"X-Crowdin-Project: beszel\n"
|
||||||
"X-Crowdin-Project-ID: 733311\n"
|
"X-Crowdin-Project-ID: 733311\n"
|
||||||
"X-Crowdin-Language: hr\n"
|
"X-Crowdin-Language: hr\n"
|
||||||
"X-Crowdin-File: /main/beszel/site/src/locales/en/en.po\n"
|
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
|
||||||
"X-Crowdin-File-ID: 16\n"
|
"X-Crowdin-File-ID: 32\n"
|
||||||
|
|
||||||
#. placeholder {0}: Math.trunc(system.info?.u / 86400)
|
#. placeholder {0}: Math.trunc(system.info?.u / 86400)
|
||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
@@ -46,7 +46,7 @@ msgstr "1 sat"
|
|||||||
#. Load average
|
#. Load average
|
||||||
#: src/components/charts/load-average-chart.tsx
|
#: src/components/charts/load-average-chart.tsx
|
||||||
msgid "1 min"
|
msgid "1 min"
|
||||||
msgstr ""
|
msgstr "1 minut"
|
||||||
|
|
||||||
#: src/lib/utils.ts
|
#: src/lib/utils.ts
|
||||||
msgid "1 week"
|
msgid "1 week"
|
||||||
@@ -59,7 +59,7 @@ msgstr "12 sati"
|
|||||||
#. Load average
|
#. Load average
|
||||||
#: src/components/charts/load-average-chart.tsx
|
#: src/components/charts/load-average-chart.tsx
|
||||||
msgid "15 min"
|
msgid "15 min"
|
||||||
msgstr ""
|
msgstr "15 minuta"
|
||||||
|
|
||||||
#: src/lib/utils.ts
|
#: src/lib/utils.ts
|
||||||
msgid "24 hours"
|
msgid "24 hours"
|
||||||
@@ -83,7 +83,7 @@ msgstr "Akcije"
|
|||||||
#: src/components/alerts-history-columns.tsx
|
#: src/components/alerts-history-columns.tsx
|
||||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||||
msgid "Active"
|
msgid "Active"
|
||||||
msgstr ""
|
msgstr "Aktivan"
|
||||||
|
|
||||||
#: src/components/routes/home.tsx
|
#: src/components/routes/home.tsx
|
||||||
msgid "Active Alerts"
|
msgid "Active Alerts"
|
||||||
@@ -141,7 +141,7 @@ msgstr "Jeste li sigurni da želite izbrisati {name}?"
|
|||||||
|
|
||||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||||
msgid "Are you sure?"
|
msgid "Are you sure?"
|
||||||
msgstr ""
|
msgstr "Jeste li sigurni?"
|
||||||
|
|
||||||
#: src/components/copy-to-clipboard.tsx
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Automatic copy requires a secure context."
|
msgid "Automatic copy requires a secure context."
|
||||||
@@ -313,7 +313,7 @@ msgstr "Kopiraj docker run"
|
|||||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||||
msgctxt "Environment variables"
|
msgctxt "Environment variables"
|
||||||
msgid "Copy env"
|
msgid "Copy env"
|
||||||
msgstr ""
|
msgstr "Kopiraj env"
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table-columns.tsx
|
#: src/components/systems-table/systems-table-columns.tsx
|
||||||
msgid "Copy host"
|
msgid "Copy host"
|
||||||
@@ -361,7 +361,7 @@ msgstr "Napravite račun"
|
|||||||
#. Context: date created
|
#. Context: date created
|
||||||
#: src/components/alerts-history-columns.tsx
|
#: src/components/alerts-history-columns.tsx
|
||||||
msgid "Created"
|
msgid "Created"
|
||||||
msgstr ""
|
msgstr "Kreiran"
|
||||||
|
|
||||||
#: src/components/routes/settings/general.tsx
|
#: src/components/routes/settings/general.tsx
|
||||||
msgid "Critical (%)"
|
msgid "Critical (%)"
|
||||||
@@ -459,12 +459,12 @@ msgstr "Preuzmi"
|
|||||||
|
|
||||||
#: src/components/alerts-history-columns.tsx
|
#: src/components/alerts-history-columns.tsx
|
||||||
msgid "Duration"
|
msgid "Duration"
|
||||||
msgstr ""
|
msgstr "Trajanje"
|
||||||
|
|
||||||
#: src/components/add-system.tsx
|
#: src/components/add-system.tsx
|
||||||
#: src/components/systems-table/systems-table-columns.tsx
|
#: src/components/systems-table/systems-table-columns.tsx
|
||||||
msgid "Edit"
|
msgid "Edit"
|
||||||
msgstr ""
|
msgstr "Uredi"
|
||||||
|
|
||||||
#: src/components/login/auth-form.tsx
|
#: src/components/login/auth-form.tsx
|
||||||
#: src/components/login/forgot-pass-form.tsx
|
#: src/components/login/forgot-pass-form.tsx
|
||||||
@@ -514,7 +514,7 @@ msgstr "Postojeći sistemi koji nisu definirani u <0>config.yml</0> će biti izb
|
|||||||
|
|
||||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||||
msgid "Export"
|
msgid "Export"
|
||||||
msgstr ""
|
msgstr "Izvezi"
|
||||||
|
|
||||||
#: src/components/routes/settings/config-yaml.tsx
|
#: src/components/routes/settings/config-yaml.tsx
|
||||||
msgid "Export configuration"
|
msgid "Export configuration"
|
||||||
@@ -549,11 +549,11 @@ msgstr "Ažuriranje upozorenja nije uspjelo"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/systems-table/systems-table.tsx
|
#: src/components/systems-table/systems-table.tsx
|
||||||
msgid "Filter..."
|
msgid "Filter..."
|
||||||
msgstr "Filter..."
|
msgstr "Filtriraj..."
|
||||||
|
|
||||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||||
msgid "Fingerprint"
|
msgid "Fingerprint"
|
||||||
msgstr ""
|
msgstr "Otisak prsta"
|
||||||
|
|
||||||
#: src/components/alerts/alerts-sheet.tsx
|
#: src/components/alerts/alerts-sheet.tsx
|
||||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||||
@@ -647,7 +647,7 @@ msgstr ""
|
|||||||
#. Short label for load average
|
#. Short label for load average
|
||||||
#: src/components/systems-table/systems-table-columns.tsx
|
#: src/components/systems-table/systems-table-columns.tsx
|
||||||
msgid "Load Avg"
|
msgid "Load Avg"
|
||||||
msgstr ""
|
msgstr "Prosječno opterećenje"
|
||||||
|
|
||||||
#: src/components/navbar.tsx
|
#: src/components/navbar.tsx
|
||||||
msgid "Log Out"
|
msgid "Log Out"
|
||||||
@@ -714,6 +714,7 @@ msgstr "Mrežni promet Docker spremnika"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr "Mrežni promet javnih sučelja"
|
msgstr "Mrežni promet javnih sučelja"
|
||||||
|
|
||||||
@@ -728,7 +729,7 @@ msgstr "Nema rezultata."
|
|||||||
|
|
||||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||||
msgid "No results."
|
msgid "No results."
|
||||||
msgstr ""
|
msgstr "Nema rezultata."
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
#: src/components/systems-table/systems-table.tsx
|
||||||
#: src/components/systems-table/systems-table.tsx
|
#: src/components/systems-table/systems-table.tsx
|
||||||
@@ -775,7 +776,7 @@ msgstr "Stranica"
|
|||||||
#. placeholder {1}: table.getPageCount()
|
#. placeholder {1}: table.getPageCount()
|
||||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||||
msgid "Page {0} of {1}"
|
msgid "Page {0} of {1}"
|
||||||
msgstr ""
|
msgstr "Stranica {0} od {1}"
|
||||||
|
|
||||||
#: src/components/command-palette.tsx
|
#: src/components/command-palette.tsx
|
||||||
msgid "Pages / Settings"
|
msgid "Pages / Settings"
|
||||||
@@ -792,7 +793,7 @@ msgstr "Lozinka mora imati najmanje 8 znakova."
|
|||||||
|
|
||||||
#: src/components/login/auth-form.tsx
|
#: src/components/login/auth-form.tsx
|
||||||
msgid "Password must be less than 72 bytes."
|
msgid "Password must be less than 72 bytes."
|
||||||
msgstr ""
|
msgstr "Lozinka mora biti kraća od 72 bajta."
|
||||||
|
|
||||||
#: src/components/login/forgot-pass-form.tsx
|
#: src/components/login/forgot-pass-form.tsx
|
||||||
msgid "Password reset request received"
|
msgid "Password reset request received"
|
||||||
@@ -808,7 +809,7 @@ msgstr "Pauzirano"
|
|||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
#: src/components/systems-table/systems-table.tsx
|
||||||
msgid "Paused ({pausedSystemsLength})"
|
msgid "Paused ({pausedSystemsLength})"
|
||||||
msgstr ""
|
msgstr "Pauzirano ({pausedSystemsLength})"
|
||||||
|
|
||||||
#: src/components/routes/settings/notifications.tsx
|
#: src/components/routes/settings/notifications.tsx
|
||||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||||
@@ -1180,7 +1181,7 @@ msgstr "Korisnici"
|
|||||||
|
|
||||||
#: src/components/alerts-history-columns.tsx
|
#: src/components/alerts-history-columns.tsx
|
||||||
msgid "Value"
|
msgid "Value"
|
||||||
msgstr ""
|
msgstr "Vrijednost"
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
#: src/components/systems-table/systems-table.tsx
|
||||||
msgid "View"
|
msgid "View"
|
||||||
@@ -1236,7 +1237,7 @@ msgstr "Piši"
|
|||||||
|
|
||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "YAML Config"
|
msgid "YAML Config"
|
||||||
msgstr "YAML Config"
|
msgstr "YAML konfiguracija"
|
||||||
|
|
||||||
#: src/components/routes/settings/config-yaml.tsx
|
#: src/components/routes/settings/config-yaml.tsx
|
||||||
msgid "YAML Configuration"
|
msgid "YAML Configuration"
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ msgstr "Docker konténerek hálózati forgalma"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr "Nyilvános interfészek hálózati forgalma"
|
msgstr "Nyilvános interfészek hálózati forgalma"
|
||||||
|
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ msgstr "Net traffík docker kerfa"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ msgstr "Traffico di rete dei container Docker"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr "Traffico di rete delle interfacce pubbliche"
|
msgstr "Traffico di rete delle interfacce pubbliche"
|
||||||
|
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ msgstr "Dockerコンテナのネットワークトラフィック"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr "パブリックインターフェースのネットワークトラフィック"
|
msgstr "パブリックインターフェースのネットワークトラフィック"
|
||||||
|
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ msgstr ""
|
|||||||
"Language: ko\n"
|
"Language: ko\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2025-08-31 15:44\n"
|
"PO-Revision-Date: 2025-09-23 02:45\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Korean\n"
|
"Language-Team: Korean\n"
|
||||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
"X-Crowdin-Project: beszel\n"
|
"X-Crowdin-Project: beszel\n"
|
||||||
"X-Crowdin-Project-ID: 733311\n"
|
"X-Crowdin-Project-ID: 733311\n"
|
||||||
"X-Crowdin-Language: ko\n"
|
"X-Crowdin-Language: ko\n"
|
||||||
"X-Crowdin-File: /main/beszel/site/src/locales/en/en.po\n"
|
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
|
||||||
"X-Crowdin-File-ID: 16\n"
|
"X-Crowdin-File-ID: 32\n"
|
||||||
|
|
||||||
#. placeholder {0}: Math.trunc(system.info?.u / 86400)
|
#. placeholder {0}: Math.trunc(system.info?.u / 86400)
|
||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
@@ -175,7 +175,7 @@ msgstr "평균 {0} 사용량"
|
|||||||
|
|
||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
msgid "Average utilization of GPU engines"
|
msgid "Average utilization of GPU engines"
|
||||||
msgstr "GPU 엔진 평균 사용률"
|
msgstr "GPU 엔진 평균 사용량"
|
||||||
|
|
||||||
#: src/components/command-palette.tsx
|
#: src/components/command-palette.tsx
|
||||||
#: src/components/navbar.tsx
|
#: src/components/navbar.tsx
|
||||||
@@ -491,7 +491,7 @@ msgstr "이메일 주소 입력..."
|
|||||||
|
|
||||||
#: src/components/login/otp-forms.tsx
|
#: src/components/login/otp-forms.tsx
|
||||||
msgid "Enter your one-time password."
|
msgid "Enter your one-time password."
|
||||||
msgstr "일회용 비밀번호를 입력하세요."
|
msgstr "OTP를 입력하세요."
|
||||||
|
|
||||||
#: src/components/login/auth-form.tsx
|
#: src/components/login/auth-form.tsx
|
||||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||||
@@ -582,7 +582,7 @@ msgstr "일반"
|
|||||||
|
|
||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
msgid "GPU Engines"
|
msgid "GPU Engines"
|
||||||
msgstr "GPU 엔진"
|
msgstr "GPU 엔진들"
|
||||||
|
|
||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
msgid "GPU Power Draw"
|
msgid "GPU Power Draw"
|
||||||
@@ -714,6 +714,7 @@ msgstr "Docker 컨테이너의 네트워크 트래픽"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr "공용 인터페이스의 네트워크 트래픽"
|
msgstr "공용 인터페이스의 네트워크 트래픽"
|
||||||
|
|
||||||
@@ -751,7 +752,7 @@ msgstr "매 시작 시, 데이터베이스가 파일에 정의된 시스템과
|
|||||||
|
|
||||||
#: src/components/login/auth-form.tsx
|
#: src/components/login/auth-form.tsx
|
||||||
msgid "One-time password"
|
msgid "One-time password"
|
||||||
msgstr "일회용 비밀번호"
|
msgstr "OTP"
|
||||||
|
|
||||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||||
@@ -873,7 +874,7 @@ msgstr "수신됨"
|
|||||||
|
|
||||||
#: src/components/login/login.tsx
|
#: src/components/login/login.tsx
|
||||||
msgid "Request a one-time password"
|
msgid "Request a one-time password"
|
||||||
msgstr "일회용 비밀번호 요청"
|
msgstr "OTP 요청"
|
||||||
|
|
||||||
#: src/components/login/otp-forms.tsx
|
#: src/components/login/otp-forms.tsx
|
||||||
msgid "Request OTP"
|
msgid "Request OTP"
|
||||||
@@ -1082,11 +1083,11 @@ msgstr "토큰과 지문은 허브에 대한 WebSocket 연결을 인증하는
|
|||||||
|
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Total data received for each interface"
|
msgid "Total data received for each interface"
|
||||||
msgstr "각 인터페이스별 총 수신 데이터량"
|
msgstr "각 인터페이스별 총합 다운로드 데이터량"
|
||||||
|
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Total data sent for each interface"
|
msgid "Total data sent for each interface"
|
||||||
msgstr "각 인터페이스별 총 발신 데이터량"
|
msgstr "각 인터페이스별 총합 업로드 데이터량"
|
||||||
|
|
||||||
#: src/lib/alerts.ts
|
#: src/lib/alerts.ts
|
||||||
msgid "Triggers when 1 minute load average exceeds a threshold"
|
msgid "Triggers when 1 minute load average exceeds a threshold"
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ msgstr "Netwerkverkeer van docker containers"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr "Netwerkverkeer van publieke interfaces"
|
msgstr "Netwerkverkeer van publieke interfaces"
|
||||||
|
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ msgstr "Nettverkstrafikk av docker-konteinere"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr "Nettverkstrafikk av eksterne nettverksgrensesnitt"
|
msgstr "Nettverkstrafikk av eksterne nettverksgrensesnitt"
|
||||||
|
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ msgstr "Ruch sieciowy kontenerów Docker."
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr "Ruch sieciowy interfejsów publicznych"
|
msgstr "Ruch sieciowy interfejsów publicznych"
|
||||||
|
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ msgstr "Tráfego de rede dos contêineres Docker"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr "Tráfego de rede das interfaces públicas"
|
msgstr "Tráfego de rede das interfaces públicas"
|
||||||
|
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ msgstr "Сетевой трафик контейнеров Docker"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr "Сетевой трафик публичных интерфейсов"
|
msgstr "Сетевой трафик публичных интерфейсов"
|
||||||
|
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ msgstr "Omrežni promet docker kontejnerjev"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr "Omrežni promet javnih vmesnikov"
|
msgstr "Omrežni promet javnih vmesnikov"
|
||||||
|
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ msgstr "Nätverkstrafik för dockercontainrar"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr "Nätverkstrafik för publika gränssnitt"
|
msgstr "Nätverkstrafik för publika gränssnitt"
|
||||||
|
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ msgstr "Docker konteynerlerinin ağ trafiği"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr "Genel arayüzlerin ağ trafiği"
|
msgstr "Genel arayüzlerin ağ trafiği"
|
||||||
|
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ msgstr "Мережевий трафік контейнерів Docker"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr "Мережевий трафік публічних інтерфейсів"
|
msgstr "Мережевий трафік публічних інтерфейсів"
|
||||||
|
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ msgstr "Lưu lượng mạng của các container Docker"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr "Lưu lượng mạng của các giao diện công cộng"
|
msgstr "Lưu lượng mạng của các giao diện công cộng"
|
||||||
|
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ msgstr "Docker 容器的网络流量"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr "公共接口的网络流量"
|
msgstr "公共接口的网络流量"
|
||||||
|
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ msgstr "Docker 容器的網絡流量"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr "公共接口的網絡流量"
|
msgstr "公共接口的網絡流量"
|
||||||
|
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ msgstr "Docker 容器的網路流量"
|
|||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
#: src/components/routes/system/network-sheet.tsx
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
|
#: src/components/routes/system/network-sheet.tsx
|
||||||
msgid "Network traffic of public interfaces"
|
msgid "Network traffic of public interfaces"
|
||||||
msgstr "公開介面的網路流量"
|
msgstr "公開介面的網路流量"
|
||||||
|
|
||||||
|
|||||||
2
internal/site/src/types.d.ts
vendored
2
internal/site/src/types.d.ts
vendored
@@ -158,6 +158,8 @@ export interface GPUData {
|
|||||||
u: number
|
u: number
|
||||||
/** power (w) */
|
/** power (w) */
|
||||||
p?: number
|
p?: number
|
||||||
|
/** power package (w) */
|
||||||
|
pp?: number
|
||||||
/** engines */
|
/** engines */
|
||||||
e?: Record<string, number>
|
e?: Record<string, number>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,25 @@
|
|||||||
|
## 0.12.12
|
||||||
|
|
||||||
|
- Fix high CPU usage when `intel_gpu_top` returns an error. (#1203)
|
||||||
|
|
||||||
|
- Add `SKIP_GPU` environment variable to skip GPU data collection. (#1203)
|
||||||
|
|
||||||
|
- Add fallback cache/buff memory calculation when cache/buff isn't available ([#1198](https://github.com/henrygd/beszel/issues/1198))
|
||||||
|
|
||||||
|
- Fix automatic agent update / restart on OpenRC. (#1199)
|
||||||
|
|
||||||
|
## 0.12.11
|
||||||
|
|
||||||
|
- Adjust calculation of cached memory (fixes #1187, #1196)
|
||||||
|
|
||||||
|
- Add pattern matching and blacklist functionality to `NICS` env var. (#1190)
|
||||||
|
|
||||||
|
- Update Intel GPU collector to parse plain text (`-l`) instead of JSON output (#1150)
|
||||||
|
|
||||||
## 0.12.10
|
## 0.12.10
|
||||||
|
|
||||||
|
Note that the default memory calculation changed in this release, which may cause a difference in memory usage compared to previous versions.
|
||||||
|
|
||||||
- Add initial support for Intel GPUs (#1150, #755)
|
- Add initial support for Intel GPUs (#1150, #755)
|
||||||
|
|
||||||
- Show connection type (WebSocket / SSH) in hub UI.
|
- Show connection type (WebSocket / SSH) in hub UI.
|
||||||
@@ -14,7 +34,6 @@
|
|||||||
|
|
||||||
- Fix divide by zero error introduced in 0.12.8 :) (#1175)
|
- Fix divide by zero error introduced in 0.12.8 :) (#1175)
|
||||||
|
|
||||||
|
|
||||||
## 0.12.8
|
## 0.12.8
|
||||||
|
|
||||||
- Add per-interface network traffic charts. (#926)
|
- Add per-interface network traffic charts. (#926)
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ StateDirectory=beszel-agent
|
|||||||
# Security/sandboxing settings
|
# Security/sandboxing settings
|
||||||
KeyringMode=private
|
KeyringMode=private
|
||||||
LockPersonality=yes
|
LockPersonality=yes
|
||||||
NoNewPrivileges=yes
|
|
||||||
PrivateTmp=yes
|
|
||||||
ProtectClock=yes
|
ProtectClock=yes
|
||||||
ProtectHome=read-only
|
ProtectHome=read-only
|
||||||
ProtectHostname=yes
|
ProtectHostname=yes
|
||||||
@@ -24,7 +22,6 @@ ProtectKernelLogs=yes
|
|||||||
ProtectSystem=strict
|
ProtectSystem=strict
|
||||||
RemoveIPC=yes
|
RemoveIPC=yes
|
||||||
RestrictSUIDSGID=true
|
RestrictSUIDSGID=true
|
||||||
SystemCallArchitectures=native
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|||||||
@@ -920,7 +920,6 @@ StateDirectory=beszel-agent
|
|||||||
# Security/sandboxing settings
|
# Security/sandboxing settings
|
||||||
KeyringMode=private
|
KeyringMode=private
|
||||||
LockPersonality=yes
|
LockPersonality=yes
|
||||||
NoNewPrivileges=yes
|
|
||||||
ProtectClock=yes
|
ProtectClock=yes
|
||||||
ProtectHome=read-only
|
ProtectHome=read-only
|
||||||
ProtectHostname=yes
|
ProtectHostname=yes
|
||||||
|
|||||||
Reference in New Issue
Block a user