mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-23 14:06:18 +01:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
||||||
|
|||||||
@@ -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,8 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type intelGpuStats struct {
|
type intelGpuStats struct {
|
||||||
Power struct {
|
PowerGPU float64
|
||||||
GPU float64 `json:"GPU"`
|
Engines map[string]float64
|
||||||
} `json:"power"`
|
|
||||||
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 +32,24 @@ 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
|
|
||||||
}
|
|
||||||
|
|
||||||
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() 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 +58,122 @@ 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.
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
_ = cmd.Wait()
|
||||||
|
}()
|
||||||
|
|
||||||
// Expect a JSON array stream: [ { ... }, { ... }, ... ]
|
scanner := bufio.NewScanner(stdout)
|
||||||
tok, err := dec.Token()
|
var header1 string
|
||||||
if err != nil {
|
var header2 string
|
||||||
return err
|
var engineNames []string
|
||||||
}
|
var friendlyNames []string
|
||||||
if delim, ok := tok.(json.Delim); !ok || delim != '[' {
|
var preEngineCols int
|
||||||
return fmt.Errorf("unexpected JSON start token: %v", tok)
|
var powerIndex int
|
||||||
}
|
|
||||||
|
|
||||||
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 err != nil {
|
if header1 == "" {
|
||||||
// When the process is still running, decoder will block in More/Decode; any error here is terminal
|
header1 = line
|
||||||
return err
|
continue
|
||||||
}
|
}
|
||||||
if delim, ok := tok.(json.Delim); ok && delim == ']' {
|
|
||||||
break
|
// second header line
|
||||||
|
if header2 == "" {
|
||||||
|
engineNames, friendlyNames, powerIndex, preEngineCols = gm.parseIntelHeaders(header1, line)
|
||||||
|
header1, header2 = "x", "x" // don't need these anymore
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data row
|
||||||
|
sample := gm.parseIntelData(line, engineNames, friendlyNames, powerIndex, preEngineCols)
|
||||||
|
gm.updateIntelFromStats(&sample)
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) == 0 {
|
||||||
|
return sample
|
||||||
|
}
|
||||||
|
// Make sure row has enough columns for engines
|
||||||
|
if need := preEngineCols + 3*len(engineNames); len(fields) < need {
|
||||||
|
return sample
|
||||||
|
}
|
||||||
|
if powerIndex >= 0 && powerIndex < len(fields) {
|
||||||
|
if v, perr := strconv.ParseFloat(fields[powerIndex], 64); perr == nil {
|
||||||
|
sample.PowerGPU = 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,13 @@ 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 two samples 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 ']')`
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -877,11 +871,215 @@ func TestIntelCollectorStreaming(t *testing.T) {
|
|||||||
|
|
||||||
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: 1.5 + 2.0 = 3.5
|
||||||
assert.InDelta(t, 3.5, gpu.Power, 0.001)
|
assert.EqualValues(t, 3.5, gpu.Power)
|
||||||
// Engines aggregated
|
// Engines aggregated
|
||||||
assert.InDelta(t, 12.34, gpu.Engines["Render/3D"], 0.001)
|
assert.EqualValues(t, 12.34, gpu.Engines["Render/3D"])
|
||||||
assert.InDelta(t, 5.0, gpu.Engines["Video"], 0.001)
|
assert.EqualValues(t, 5.0, gpu.Engines["Video"])
|
||||||
|
assert.EqualValues(t, 0.0, gpu.Engines["Blitter"]) // BCS is zero in both samples
|
||||||
// Count should be 2 samples
|
// Count should be 2 samples
|
||||||
assert.Equal(t, float64(2), gpu.Count)
|
assert.Equal(t, float64(2), 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
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty line",
|
||||||
|
line: "",
|
||||||
|
engineNames: []string{"RCS"},
|
||||||
|
friendlyNames: []string{"Render/3D"},
|
||||||
|
powerIndex: 4,
|
||||||
|
preEngineCols: 8,
|
||||||
|
wantPowerGPU: 0.0,
|
||||||
|
wantEngines: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 := gm.parseIntelData(tt.line, tt.engineNames, tt.friendlyNames, tt.powerIndex, tt.preEngineCols)
|
||||||
|
|
||||||
|
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,19 @@ 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
|
||||||
|
// 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 {
|
||||||
|
|||||||
@@ -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.11"
|
||||||
// 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
|
||||||
|
|
||||||
|
|||||||
@@ -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.11",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "beszel",
|
"name": "beszel",
|
||||||
"version": "0.12.10",
|
"version": "0.12.11",
|
||||||
"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.11",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host",
|
"dev": "vite --host",
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export default memo(function GpuPowerChart({ chartData }: { chartData: ChartData
|
|||||||
}
|
}
|
||||||
const keys = Object.keys(powerSums).sort((a, b) => powerSums[b] - powerSums[a])
|
const keys = Object.keys(powerSums).sort((a, b) => powerSums[b] - powerSums[a])
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
newChartData.colors[key] = `hsl(${((keys.indexOf(key) * 360) / keys.length) % 360}, 60%, 55%)`
|
newChartData.colors[key] = `hsl(${(226 + (keys.indexOf(key) * 360) / keys.length) % 360}, 60%, 55%)`
|
||||||
}
|
}
|
||||||
return newChartData
|
return newChartData
|
||||||
}, [chartData])
|
}, [chartData])
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -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 "公開介面的網路流量"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
|
## 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 +24,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)
|
||||||
|
|||||||
Reference in New Issue
Block a user