Compare commits

...

55 Commits

Author SHA1 Message Date
Shelby Tucker
40b3951615 [Feature] Basic systemd service monitoring (#1153)
* basic systemd service monitoring

* update to work after /internal rename

* monitor systemd service cpu and memory usage

---------

Co-authored-by: henrygd <hank@henrygd.me>
2025-11-10 15:29:21 -05:00
henrygd
ca58ff66ba 0.12.12 release 2025-09-25 19:37:26 -04:00
henrygd
133d229361 add fallback cache/buff memory calculation when cache/buff isn't available (#1198) 2025-09-25 19:19:32 -04:00
henrygd
960cac4060 fix intel_gpu_top restart loop and add intel gpu pkg power (#1203) 2025-09-25 19:15:36 -04:00
henrygd
d83865cb4f remove NoNewPrivileges from systemd agent service
configuration (#1203)

Prevents service from running `intel_gpu_top`
2025-09-25 15:06:17 -04:00
henrygd
4b43d68da6 add SKIP_GPU=true (#1203) 2025-09-25 14:10:28 -04:00
henrygd
c790d76211 fix command arguments for OpenRC agent restart functionality (#1199) 2025-09-24 23:14:15 -04:00
henrygd
29b182fd7b 0.12.11 release :) 2025-09-24 18:08:54 -04:00
henrygd
fc78b959aa update colors for gpu power chart 2025-09-24 18:01:51 -04:00
henrygd
b8b3604aec update language files 2025-09-24 17:41:11 -04:00
henrygd
e45606fdec New Croatian translations by nikola.smis on Crowdin 2025-09-24 17:40:22 -04:00
aroxu
640afd82ad New Korean translations 2025-09-24 17:31:04 -04:00
henrygd
d025e51c67 make sure agent connection title works in grid layout 2025-09-24 17:15:17 -04:00
henrygd
f70c30345a fix sticky header z-index 2025-09-24 17:07:38 -04:00
henrygd
63bdac83a1 hide interfaces chart legend if interfaces.length > 15 2025-09-24 16:45:43 -04:00
Sven van Ginkel
65897a8df6 add cali to the default nics skip list (#1195) 2025-09-24 16:29:11 -04:00
henrygd
0dc9b3e273 add pattern matching and blacklist functionality to NICS env var. (#1190) 2025-09-24 16:27:37 -04:00
Sven van Ginkel
c1c0d8d672 Fix hub executable (#1193) 2025-09-24 15:13:24 -04:00
henrygd
1811ab64be add migration to fix bad cached mem values (#1196) 2025-09-24 15:07:11 -04:00
henrygd
5578520054 add title to agent connection type in all systems table 2025-09-24 14:18:20 -04:00
henrygd
7b128d09ac Update Intel GPU collector to parse plain text (-l) instead of JSON output (#1150) 2025-09-24 13:24:48 -04:00
henrygd
d295507c0b adjust calculation of cached memory (#1187, #1196) 2025-09-24 13:23:59 -04:00
henrygd
79fbbb7ad0 add ghcr.io image configuration for beszel-agent-intel in gh workflow 2025-09-23 20:16:19 -04:00
henrygd
e7325b23c4 simplify filter bar component 2025-09-23 20:15:42 -04:00
henrygd
c5eba6547a comments 2025-09-22 20:48:37 -04:00
henrygd
82e7c04b25 0.12.10 release :) 2025-09-22 18:32:30 -04:00
henrygd
a9ce16cfdd update language files 2025-09-22 18:28:39 -04:00
henrygd
2af8b6057f new Polish translations 2025-09-22 18:18:39 -04:00
henrygd
3fae4360a8 update changelog 2025-09-22 18:14:36 -04:00
henrygd
10073d85e1 update go deps 2025-09-22 18:13:50 -04:00
henrygd
e240ced018 add support for henrygd/beszel-agent-intel in docker workflow 2025-09-22 17:47:32 -04:00
henrygd
ae1e17f5ed add dockerfile for henrygd/beszel-agent-intel 2025-09-22 17:41:44 -04:00
henrygd
3abb7c213b initial support for one intel gpu with intel_gpu_top 2025-09-22 16:36:10 -04:00
henrygd
240e75f025 add sorted style to home table header buttons 2025-09-21 19:23:34 -04:00
henrygd
ea984844ff update changelog 2025-09-21 17:56:35 -04:00
henrygd
0d157b5857 display agent connection type in hub (ssh, websocket) 2025-09-21 17:49:22 -04:00
henrygd
d0b6e725c8 fix positioning of bandwidth chart button 2025-09-19 12:08:41 -04:00
henrygd
ffe7f8547a fix: update temperature and byte formatting functions to use loose equality checks (#1180) 2025-09-19 11:51:27 -04:00
henrygd
37817b0f15 add --auto-update flag to hub install script 2025-09-18 17:51:22 -04:00
henrygd
a66ac418ae install: remove additional service restart for openwrt 2025-09-18 14:05:19 -04:00
henrygd
2ee2f53267 fix: resolve mipsle architecture detection for install script (#1176)
- Add proper endianness detection using ELF header inspection
- Prevent mipsle devices from downloading incorrect mips binaries
- Maintain backward compatibility for all other architectures
2025-09-18 13:48:24 -04:00
henrygd
e5c766c00b refactoring
- network interface delta
- string concatenation
2025-09-17 21:36:05 -04:00
henrygd
da43ba10e1 add aria-label to button in NetworkSheet for improved accessibility 2025-09-17 16:23:02 -04:00
henrygd
fca13004bd release 0.12.9 2025-09-17 16:06:20 -04:00
henrygd
376a86829c fix divide by zero error (#1175) 2025-09-17 16:00:50 -04:00
henrygd
ef48613f3f improve style of chart sheet button on mobile
- also update changelog
2025-09-17 15:13:10 -04:00
henrygd
49976c6f61 fix nvidia agent dockerfile after project reorganization 2025-09-17 14:10:02 -04:00
henrygd
d68f1f0985 0.12.8 release :) 2025-09-17 14:02:17 -04:00
henrygd
273a090200 update translations 2025-09-17 14:01:20 -04:00
henrygd
59057a2ba4 add check for status alerts which are not properly resolved (#1052) 2025-09-17 13:31:49 -04:00
henrygd
1b9e781d45 refactor deltatracker
- embed mutex
- add example function
2025-09-16 22:09:46 -04:00
henrygd
4e0ca7c2ba formatting (biome) 2025-09-15 18:04:13 -04:00
henrygd
a9e7bcd37f add per-interface and cumulative network traffic charts (#926)
Co-authored-by: Sven van Ginkel <svenvanginkel@icloud.com>
2025-09-15 17:59:21 -04:00
henrygd
4635f24fb2 fix entre arg in makefile dev server 2025-09-15 17:26:07 -04:00
henrygd
3e73399b87 fix battery detection on newer macs (#1170) 2025-09-15 12:02:50 -04:00
105 changed files with 5636 additions and 1015 deletions

View File

@@ -33,6 +33,14 @@ jobs:
registry: docker.io
username_secret: DOCKERHUB_USERNAME
password_secret: DOCKERHUB_TOKEN
- image: henrygd/beszel-agent-intel
context: ./
dockerfile: ./internal/dockerfile_agent_intel
platforms: linux/amd64
registry: docker.io
username_secret: DOCKERHUB_USERNAME
password_secret: DOCKERHUB_TOKEN
- image: ghcr.io/${{ github.repository }}/beszel
context: ./
@@ -56,6 +64,14 @@ jobs:
username: ${{ github.actor }}
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:
contents: read
packages: write

View File

@@ -77,7 +77,7 @@ dev-hub: export ENV=dev
dev-hub:
mkdir -p ./internal/site/dist && touch ./internal/site/dist/index.html
@if command -v entr >/dev/null 2>&1; then \
find ./internal/cmd/hub/*.go ./internal/{alerts,hub,records,users}/*.go | entr -r -s "cd ./internal/cmd/hub && go run -tags development . serve --http 0.0.0.0:8090"; \
find ./internal -type f -name '*.go' | entr -r -s "cd ./internal/cmd/hub && go run -tags development . serve --http 0.0.0.0:8090"; \
else \
cd ./internal/cmd/hub && go run -tags development . serve --http 0.0.0.0:8090; \
fi

View File

@@ -31,6 +31,7 @@ type Agent struct {
netInterfaces map[string]struct{} // Stores all valid network interfaces
netIoStats system.NetIoStats // Keeps track of bandwidth usage
dockerManager *dockerManager // Manages Docker API requests
systemdManager *systemdManager // Manages systemd services
sensorConfig *SensorConfig // Sensors config
systemInfo system.Info // Host system info
gpuManager *GPUManager // Manages GPU data
@@ -88,6 +89,13 @@ func NewAgent(dataDir ...string) (agent *Agent, err error) {
// initialize docker manager
agent.dockerManager = newDockerManager(agent)
// initialize systemd manager
if sm, err := newSystemdManager(); err != nil {
slog.Debug("Systemd", "err", err)
} else {
agent.systemdManager = sm
}
// initialize GPU manager
if gm, err := NewGPUManager(); err != nil {
slog.Debug("GPU", "err", err)
@@ -137,6 +145,11 @@ func (a *Agent) gatherStats(sessionID string) *system.CombinedData {
}
}
if a.systemdManager != nil {
data.SystemdServices = a.systemdManager.getServiceStats()
slog.Debug("Systemd services", "data", data.SystemdServices)
}
data.Stats.ExtraFs = make(map[string]*system.FsStats)
for name, stats := range a.fsStats {
if !stats.Root && stats.DiskTotal > 0 {

View File

@@ -20,9 +20,8 @@ func HasReadableBattery() bool {
}
haveCheckedBattery = true
bat, err := battery.Get(0)
if err == nil && bat != nil {
systemHasBattery = true
} else {
systemHasBattery = err == nil && bat != nil && bat.Design != 0 && bat.Full != 0
if !systemHasBattery {
slog.Debug("No battery found", "err", err)
}
return systemHasBattery

View File

@@ -9,19 +9,21 @@ import (
"time"
"github.com/henrygd/beszel/agent/health"
"github.com/henrygd/beszel/internal/entities/system"
)
// ConnectionManager manages the connection state and events for the agent.
// It handles both WebSocket and SSH connections, automatically switching between
// them based on availability and managing reconnection attempts.
type ConnectionManager struct {
agent *Agent // Reference to the parent agent
State ConnectionState // Current connection state
eventChan chan ConnectionEvent // Channel for connection events
wsClient *WebSocketClient // WebSocket client for hub communication
serverOptions ServerOptions // Configuration for SSH server
wsTicker *time.Ticker // Ticker for WebSocket connection attempts
isConnecting bool // Prevents multiple simultaneous reconnection attempts
agent *Agent // Reference to the parent agent
State ConnectionState // Current connection state
eventChan chan ConnectionEvent // Channel for connection events
wsClient *WebSocketClient // WebSocket client for hub communication
serverOptions ServerOptions // Configuration for SSH server
wsTicker *time.Ticker // Ticker for WebSocket connection attempts
isConnecting bool // Prevents multiple simultaneous reconnection attempts
ConnectionType system.ConnectionType
}
// ConnectionState represents the current connection state of the agent.
@@ -144,15 +146,18 @@ func (c *ConnectionManager) handleStateChange(newState ConnectionState) {
switch newState {
case WebSocketConnected:
slog.Info("WebSocket connected", "host", c.wsClient.hubURL.Host)
c.ConnectionType = system.ConnectionTypeWebSocket
c.stopWsTicker()
_ = c.agent.StopServer()
c.isConnecting = false
case SSHConnected:
// stop new ws connection attempts
slog.Info("SSH connection established")
c.ConnectionType = system.ConnectionTypeSSH
c.stopWsTicker()
c.isConnecting = false
case Disconnected:
c.ConnectionType = system.ConnectionTypeNone
if c.isConnecting {
// Already handling reconnection, avoid duplicate attempts
return

View File

@@ -0,0 +1,81 @@
// Package deltatracker provides a tracker for calculating differences in numeric values over time.
package deltatracker
import (
"sync"
"golang.org/x/exp/constraints"
)
// Numeric is a constraint that permits any integer or floating-point type.
type Numeric interface {
constraints.Integer | constraints.Float
}
// DeltaTracker is a generic, thread-safe tracker for calculating differences
// in numeric values over time.
// K is the key type (e.g., int, string).
// V is the value type (e.g., int, int64, float32, float64).
type DeltaTracker[K comparable, V Numeric] struct {
sync.RWMutex
current map[K]V
previous map[K]V
}
// NewDeltaTracker creates a new generic tracker.
func NewDeltaTracker[K comparable, V Numeric]() *DeltaTracker[K, V] {
return &DeltaTracker[K, V]{
current: make(map[K]V),
previous: make(map[K]V),
}
}
// Set records the current value for a given ID.
func (t *DeltaTracker[K, V]) Set(id K, value V) {
t.Lock()
defer t.Unlock()
t.current[id] = value
}
// Deltas returns a map of all calculated deltas for the current interval.
func (t *DeltaTracker[K, V]) Deltas() map[K]V {
t.RLock()
defer t.RUnlock()
deltas := make(map[K]V)
for id, currentVal := range t.current {
if previousVal, ok := t.previous[id]; ok {
deltas[id] = currentVal - previousVal
} else {
deltas[id] = 0
}
}
return deltas
}
// Delta returns the delta for a single key.
// Returns 0 if the key doesn't exist or has no previous value.
func (t *DeltaTracker[K, V]) Delta(id K) V {
t.RLock()
defer t.RUnlock()
currentVal, currentOk := t.current[id]
if !currentOk {
return 0
}
previousVal, previousOk := t.previous[id]
if !previousOk {
return 0
}
return currentVal - previousVal
}
// Cycle prepares the tracker for the next interval.
func (t *DeltaTracker[K, V]) Cycle() {
t.Lock()
defer t.Unlock()
t.previous = t.current
t.current = make(map[K]V)
}

View File

@@ -0,0 +1,217 @@
package deltatracker
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func ExampleDeltaTracker() {
tracker := NewDeltaTracker[string, int]()
tracker.Set("key1", 10)
tracker.Set("key2", 20)
tracker.Cycle()
tracker.Set("key1", 15)
tracker.Set("key2", 30)
fmt.Println(tracker.Delta("key1"))
fmt.Println(tracker.Delta("key2"))
fmt.Println(tracker.Deltas())
// Output: 5
// 10
// map[key1:5 key2:10]
}
func TestNewDeltaTracker(t *testing.T) {
tracker := NewDeltaTracker[string, int]()
assert.NotNil(t, tracker)
assert.Empty(t, tracker.current)
assert.Empty(t, tracker.previous)
}
func TestSet(t *testing.T) {
tracker := NewDeltaTracker[string, int]()
tracker.Set("key1", 10)
tracker.RLock()
defer tracker.RUnlock()
assert.Equal(t, 10, tracker.current["key1"])
}
func TestDeltas(t *testing.T) {
tracker := NewDeltaTracker[string, int]()
// Test with no previous values
tracker.Set("key1", 10)
tracker.Set("key2", 20)
deltas := tracker.Deltas()
assert.Equal(t, 0, deltas["key1"])
assert.Equal(t, 0, deltas["key2"])
// Cycle to move current to previous
tracker.Cycle()
// Set new values and check deltas
tracker.Set("key1", 15) // Delta should be 5 (15-10)
tracker.Set("key2", 25) // Delta should be 5 (25-20)
tracker.Set("key3", 30) // New key, delta should be 0
deltas = tracker.Deltas()
assert.Equal(t, 5, deltas["key1"])
assert.Equal(t, 5, deltas["key2"])
assert.Equal(t, 0, deltas["key3"])
}
func TestCycle(t *testing.T) {
tracker := NewDeltaTracker[string, int]()
tracker.Set("key1", 10)
tracker.Set("key2", 20)
// Verify current has values
tracker.RLock()
assert.Equal(t, 10, tracker.current["key1"])
assert.Equal(t, 20, tracker.current["key2"])
assert.Empty(t, tracker.previous)
tracker.RUnlock()
tracker.Cycle()
// After cycle, previous should have the old current values
// and current should be empty
tracker.RLock()
assert.Empty(t, tracker.current)
assert.Equal(t, 10, tracker.previous["key1"])
assert.Equal(t, 20, tracker.previous["key2"])
tracker.RUnlock()
}
func TestCompleteWorkflow(t *testing.T) {
tracker := NewDeltaTracker[string, int]()
// First interval
tracker.Set("server1", 100)
tracker.Set("server2", 200)
// Get deltas for first interval (should be zero)
firstDeltas := tracker.Deltas()
assert.Equal(t, 0, firstDeltas["server1"])
assert.Equal(t, 0, firstDeltas["server2"])
// Cycle to next interval
tracker.Cycle()
// Second interval
tracker.Set("server1", 150) // Delta: 50
tracker.Set("server2", 180) // Delta: -20
tracker.Set("server3", 300) // New server, delta: 300
secondDeltas := tracker.Deltas()
assert.Equal(t, 50, secondDeltas["server1"])
assert.Equal(t, -20, secondDeltas["server2"])
assert.Equal(t, 0, secondDeltas["server3"])
}
func TestDeltaTrackerWithDifferentTypes(t *testing.T) {
// Test with int64
intTracker := NewDeltaTracker[string, int64]()
intTracker.Set("pid1", 1000)
intTracker.Cycle()
intTracker.Set("pid1", 1200)
intDeltas := intTracker.Deltas()
assert.Equal(t, int64(200), intDeltas["pid1"])
// Test with float64
floatTracker := NewDeltaTracker[string, float64]()
floatTracker.Set("cpu1", 1.5)
floatTracker.Cycle()
floatTracker.Set("cpu1", 2.7)
floatDeltas := floatTracker.Deltas()
assert.InDelta(t, 1.2, floatDeltas["cpu1"], 0.0001)
// Test with int keys
pidTracker := NewDeltaTracker[int, int64]()
pidTracker.Set(101, 20000)
pidTracker.Cycle()
pidTracker.Set(101, 22500)
pidDeltas := pidTracker.Deltas()
assert.Equal(t, int64(2500), pidDeltas[101])
}
func TestDelta(t *testing.T) {
tracker := NewDeltaTracker[string, int]()
// Test getting delta for non-existent key
result := tracker.Delta("nonexistent")
assert.Equal(t, 0, result)
// Test getting delta for key with no previous value
tracker.Set("key1", 10)
result = tracker.Delta("key1")
assert.Equal(t, 0, result)
// Cycle to move current to previous
tracker.Cycle()
// Test getting delta for key with previous value
tracker.Set("key1", 15)
result = tracker.Delta("key1")
assert.Equal(t, 5, result)
// Test getting delta for key that exists in previous but not current
result = tracker.Delta("key1")
assert.Equal(t, 5, result) // Should still return 5
// Test getting delta for key that exists in current but not previous
tracker.Set("key2", 20)
result = tracker.Delta("key2")
assert.Equal(t, 0, result)
}
func TestDeltaWithDifferentTypes(t *testing.T) {
// Test with int64
intTracker := NewDeltaTracker[string, int64]()
intTracker.Set("pid1", 1000)
intTracker.Cycle()
intTracker.Set("pid1", 1200)
result := intTracker.Delta("pid1")
assert.Equal(t, int64(200), result)
// Test with float64
floatTracker := NewDeltaTracker[string, float64]()
floatTracker.Set("cpu1", 1.5)
floatTracker.Cycle()
floatTracker.Set("cpu1", 2.7)
floatResult := floatTracker.Delta("cpu1")
assert.InDelta(t, 1.2, floatResult, 0.0001)
// Test with int keys
pidTracker := NewDeltaTracker[int, int64]()
pidTracker.Set(101, 20000)
pidTracker.Cycle()
pidTracker.Set(101, 22500)
pidResult := pidTracker.Delta(101)
assert.Equal(t, int64(2500), pidResult)
}
func TestDeltaConcurrentAccess(t *testing.T) {
tracker := NewDeltaTracker[string, int]()
// Set initial values
tracker.Set("key1", 10)
tracker.Set("key2", 20)
tracker.Cycle()
// Set new values
tracker.Set("key1", 15)
tracker.Set("key2", 25)
// Test concurrent access safety
result1 := tracker.Delta("key1")
result2 := tracker.Delta("key2")
assert.Equal(t, 5, result1)
assert.Equal(t, 5, result2)
}

View File

@@ -27,13 +27,10 @@ const (
nvidiaSmiInterval string = "4" // in seconds
tegraStatsInterval string = "3700" // in milliseconds
rocmSmiInterval time.Duration = 4300 * time.Millisecond
// Command retry and timeout constants
retryWaitTime time.Duration = 5 * time.Second
maxFailureRetries int = 5
cmdBufferSize uint16 = 10 * 1024
// Unit Conversions
mebibytesInAMegabyte float64 = 1.024 // nvidia-smi reports memory in MiB
milliwattsInAWatt float64 = 1000.0 // tegrastats reports power in mW
@@ -42,10 +39,11 @@ const (
// GPUManager manages data collection for GPUs (either Nvidia or AMD)
type GPUManager struct {
sync.Mutex
nvidiaSmi bool
rocmSmi bool
tegrastats bool
GpuDataMap map[string]*system.GPUData
nvidiaSmi bool
rocmSmi bool
tegrastats bool
intelGpuStats bool
GpuDataMap map[string]*system.GPUData
}
// RocmSmiJson represents the JSON structure of rocm-smi output
@@ -66,6 +64,7 @@ type gpuCollector struct {
cmdArgs []string
parse func([]byte) bool // returns true if valid data was found
buf []byte
bufSize uint16
}
var errNoValidData = fmt.Errorf("no valid GPU data found") // Error for missing data
@@ -99,7 +98,7 @@ func (c *gpuCollector) collect() error {
scanner := bufio.NewScanner(stdout)
if c.buf == nil {
c.buf = make([]byte, 0, cmdBufferSize)
c.buf = make([]byte, 0, c.bufSize)
}
scanner.Buffer(c.buf, bufio.MaxScanTokenSize)
@@ -244,20 +243,32 @@ func (gm *GPUManager) GetCurrentData() map[string]system.GPUData {
// copy / reset the data
gpuData := make(map[string]system.GPUData, len(gm.GpuDataMap))
for id, gpu := range gm.GpuDataMap {
gpuAvg := *gpu
gpuAvg.Temperature = twoDecimals(gpu.Temperature)
gpuAvg.MemoryUsed = twoDecimals(gpu.MemoryUsed)
gpuAvg.MemoryTotal = twoDecimals(gpu.MemoryTotal)
// avoid division by zero
if gpu.Count > 0 {
gpuAvg.Usage = twoDecimals(gpu.Usage / gpu.Count)
gpuAvg.Power = twoDecimals(gpu.Power / gpu.Count)
count := max(gpu.Count, 1)
// average the data
gpuAvg := *gpu
gpuAvg.Temperature = twoDecimals(gpu.Temperature)
gpuAvg.Power = twoDecimals(gpu.Power / count)
// intel gpu stats doesn't provide usage, memory used, or memory total
if gpu.Engines != nil {
maxEngineUsage := 0.0
for name, engine := range gpu.Engines {
gpuAvg.Engines[name] = twoDecimals(engine / count)
maxEngineUsage = max(maxEngineUsage, engine/count)
}
gpuAvg.PowerPkg = twoDecimals(gpu.PowerPkg / count)
gpuAvg.Usage = twoDecimals(maxEngineUsage)
} else {
gpuAvg.Usage = twoDecimals(gpu.Usage / count)
gpuAvg.MemoryUsed = twoDecimals(gpu.MemoryUsed)
gpuAvg.MemoryTotal = twoDecimals(gpu.MemoryTotal)
}
// reset accumulators in the original
gpu.Usage, gpu.Power, gpu.Count = 0, 0, 0
// reset accumulators in the original gpu data for next collection
gpu.Usage, gpu.Power, gpu.PowerPkg, gpu.Count = gpuAvg.Usage, gpuAvg.Power, gpuAvg.PowerPkg, 1
gpu.Engines = gpuAvg.Engines
// append id to the name if there are multiple GPUs with the same name
if nameCounts[gpu.Name] > 1 {
@@ -284,18 +295,37 @@ func (gm *GPUManager) detectGPUs() error {
gm.tegrastats = true
gm.nvidiaSmi = false
}
if gm.nvidiaSmi || gm.rocmSmi || gm.tegrastats {
if _, err := exec.LookPath(intelGpuStatsCmd); err == nil {
gm.intelGpuStats = true
}
if gm.nvidiaSmi || gm.rocmSmi || gm.tegrastats || gm.intelGpuStats {
return nil
}
return fmt.Errorf("no GPU found - install nvidia-smi, rocm-smi, or tegrastats")
return fmt.Errorf("no GPU found - install nvidia-smi, rocm-smi, tegrastats, or intel_gpu_top")
}
// startCollector starts the appropriate GPU data collector based on the command
func (gm *GPUManager) startCollector(command string) {
collector := gpuCollector{
name: command,
name: command,
bufSize: 10 * 1024,
}
switch command {
case intelGpuStatsCmd:
go func() {
failures := 0
for {
if err := gm.collectIntelStats(); err != nil {
failures++
if failures > maxFailureRetries {
break
}
slog.Warn("Error collecting Intel GPU data; see https://beszel.dev/guide/gpu", "err", err)
time.Sleep(retryWaitTime)
continue
}
}
}()
case nvidiaSmiCmd:
collector.cmdArgs = []string{
"-l", nvidiaSmiInterval,
@@ -329,6 +359,9 @@ func (gm *GPUManager) startCollector(command string) {
// NewGPUManager creates and initializes a new GPUManager
func NewGPUManager() (*GPUManager, error) {
if skipGPU, _ := GetEnv("SKIP_GPU"); skipGPU == "true" {
return nil, nil
}
var gm GPUManager
if err := gm.detectGPUs(); err != nil {
return nil, err
@@ -344,6 +377,9 @@ func NewGPUManager() (*GPUManager, error) {
if gm.tegrastats {
gm.startCollector(tegraStatsCmd)
}
if gm.intelGpuStats {
gm.startCollector(intelGpuStatsCmd)
}
return &gm, nil
}

199
agent/gpu_intel.go Normal file
View File

@@ -0,0 +1,199 @@
package agent
import (
"bufio"
"io"
"os/exec"
"strconv"
"strings"
"github.com/henrygd/beszel/internal/entities/system"
)
const (
intelGpuStatsCmd string = "intel_gpu_top"
intelGpuStatsInterval string = "3300" // in milliseconds
)
type intelGpuStats struct {
PowerGPU float64
PowerPkg float64
Engines map[string]float64
}
// updateIntelFromStats updates aggregated GPU data from a single intelGpuStats sample
func (gm *GPUManager) updateIntelFromStats(sample *intelGpuStats) bool {
gm.Lock()
defer gm.Unlock()
// only one gpu for now - cmd doesn't provide all by default
gpuData, ok := gm.GpuDataMap["0"]
if !ok {
gpuData = &system.GPUData{Name: "GPU", Engines: make(map[string]float64)}
gm.GpuDataMap["0"] = gpuData
}
gpuData.Power += sample.PowerGPU
gpuData.PowerPkg += sample.PowerPkg
if gpuData.Engines == nil {
gpuData.Engines = make(map[string]float64, len(sample.Engines))
}
for name, engine := range sample.Engines {
gpuData.Engines[name] += engine
}
gpuData.Count++
return true
}
// collectIntelStats executes intel_gpu_top in text mode (-l) and parses the output
func (gm *GPUManager) collectIntelStats() (err error) {
cmd := exec.Command(intelGpuStatsCmd, "-s", intelGpuStatsInterval, "-l")
// Avoid blocking if intel_gpu_top writes to stderr
cmd.Stderr = io.Discard
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
// 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
}
}()
scanner := bufio.NewScanner(stdout)
var header1 string
var engineNames []string
var friendlyNames []string
var preEngineCols int
var powerIndex int
var hadDataRow bool
// skip first data row because it sometimes has erroneous data
var skippedFirstDataRow bool
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
// 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 {
return err
}
hadDataRow = true
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
}
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
}

View File

@@ -379,12 +379,12 @@ func TestGetCurrentData(t *testing.T) {
assert.InDelta(t, 60.0, result["1"].Power, 0.01)
// Verify that accumulators in the original map are reset
assert.Equal(t, float64(0), gm.GpuDataMap["0"].Count, "GPU 0 Count should be reset")
assert.Equal(t, float64(0), gm.GpuDataMap["0"].Usage, "GPU 0 Usage should be reset")
assert.Equal(t, float64(0), gm.GpuDataMap["0"].Power, "GPU 0 Power should be reset")
assert.Equal(t, float64(0), gm.GpuDataMap["1"].Count, "GPU 1 Count should be reset")
assert.Equal(t, float64(0), gm.GpuDataMap["1"].Usage, "GPU 1 Usage should be reset")
assert.Equal(t, float64(0), gm.GpuDataMap["1"].Power, "GPU 1 Power should be reset")
assert.EqualValues(t, float64(1), gm.GpuDataMap["0"].Count, "GPU 0 Count should be reset")
assert.EqualValues(t, float64(50.0), gm.GpuDataMap["0"].Usage, "GPU 0 Usage should be reset")
assert.Equal(t, float64(100.0), gm.GpuDataMap["0"].Power, "GPU 0 Power should be reset")
assert.Equal(t, float64(1), gm.GpuDataMap["1"].Count, "GPU 1 Count should be reset")
assert.Equal(t, float64(30), gm.GpuDataMap["1"].Usage, "GPU 1 Usage should be reset")
assert.Equal(t, float64(60), gm.GpuDataMap["1"].Power, "GPU 1 Power should be reset")
})
t.Run("handles zero count without panicking", func(t *testing.T) {
@@ -409,7 +409,7 @@ func TestGetCurrentData(t *testing.T) {
assert.Equal(t, 0.0, result["0"].Power)
// Verify reset count
assert.Equal(t, float64(0), gm.GpuDataMap["0"].Count)
assert.EqualValues(t, 1, gm.GpuDataMap["0"].Count)
})
}
@@ -756,11 +756,11 @@ func TestAccumulation(t *testing.T) {
continue
}
assert.InDelta(t, expected.temperature, gpu.Temperature, 0.01, "Temperature should match")
assert.InDelta(t, expected.memoryUsed, gpu.MemoryUsed, 0.01, "Memory used should match")
assert.InDelta(t, expected.memoryTotal, gpu.MemoryTotal, 0.01, "Memory total should match")
assert.InDelta(t, expected.usage, gpu.Usage, 0.01, "Usage should match")
assert.InDelta(t, expected.power, gpu.Power, 0.01, "Power should match")
assert.EqualValues(t, expected.temperature, gpu.Temperature, "Temperature should match")
assert.EqualValues(t, expected.memoryUsed, gpu.MemoryUsed, "Memory used should match")
assert.EqualValues(t, expected.memoryTotal, gpu.MemoryTotal, "Memory total should match")
assert.EqualValues(t, expected.usage, gpu.Usage, "Usage should match")
assert.EqualValues(t, expected.power, gpu.Power, "Power should match")
assert.Equal(t, expected.count, gpu.Count, "Count should match")
}
@@ -773,22 +773,320 @@ func TestAccumulation(t *testing.T) {
continue
}
assert.InDelta(t, expected.temperature, gpu.Temperature, 0.01, "Temperature in GetCurrentData should match")
assert.InDelta(t, expected.avgUsage, gpu.Usage, 0.01, "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.temperature, gpu.Temperature, "Temperature in GetCurrentData should match")
assert.EqualValues(t, expected.avgUsage, gpu.Usage, "Average usage 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
for id := range tt.expectedValues {
for id, expected := range tt.expectedValues {
gpu, exists := gm.GpuDataMap[id]
assert.True(t, exists, "GPU with ID %s should still exist after GetCurrentData", id)
if !exists {
continue
}
assert.Equal(t, float64(0), gpu.Count, "Count should be reset for GPU ID %s", id)
assert.Equal(t, float64(0), gpu.Usage, "Usage should be reset for GPU ID %s", id)
assert.Equal(t, float64(0), gpu.Power, "Power should be reset for GPU ID %s", id)
assert.EqualValues(t, 1, gpu.Count, "Count should be reset for GPU ID %s", id)
assert.EqualValues(t, expected.avgUsage, gpu.Usage, "Usage should be reset for GPU ID %s", id)
assert.EqualValues(t, expected.avgPower, gpu.Power, "Power should be reset for GPU ID %s", id)
}
})
}
}
func TestIntelUpdateFromStats(t *testing.T) {
gm := &GPUManager{
GpuDataMap: make(map[string]*system.GPUData),
}
// First sample with power and two engines
sample1 := intelGpuStats{
PowerGPU: 10.5,
Engines: map[string]float64{
"Render/3D": 20.0,
"Video": 5.0,
},
}
ok := gm.updateIntelFromStats(&sample1)
assert.True(t, ok)
gpu := gm.GpuDataMap["0"]
require.NotNil(t, gpu)
assert.Equal(t, "GPU", gpu.Name)
assert.EqualValues(t, 10.5, gpu.Power)
assert.EqualValues(t, 20.0, gpu.Engines["Render/3D"])
assert.EqualValues(t, 5.0, gpu.Engines["Video"])
assert.Equal(t, float64(1), gpu.Count)
// Second sample with zero power (should not add) and additional engine busy
sample2 := intelGpuStats{
PowerGPU: 0.0,
Engines: map[string]float64{
"Render/3D": 10.0,
"Video": 2.5,
"Blitter": 1.0,
},
}
// zero power should not increment power accumulator
ok = gm.updateIntelFromStats(&sample2)
assert.True(t, ok)
gpu = gm.GpuDataMap["0"]
require.NotNil(t, gpu)
assert.EqualValues(t, 10.5, gpu.Power)
assert.EqualValues(t, 30.0, gpu.Engines["Render/3D"]) // 20 + 10
assert.EqualValues(t, 7.5, gpu.Engines["Video"]) // 5 + 2.5
assert.EqualValues(t, 1.0, gpu.Engines["Blitter"])
assert.Equal(t, float64(2), gpu.Count)
}
func TestIntelCollectorStreaming(t *testing.T) {
// Save and override PATH
origPath := os.Getenv("PATH")
defer os.Setenv("PATH", origPath)
dir := t.TempDir()
os.Setenv("PATH", dir)
// 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")
script := `#!/bin/sh
echo "Freq MHz IRQ RC6 Power W IMC MiB/s RCS BCS VCS"
echo " req act /s % gpu pkg rd wr % se wa % se wa % se wa"
echo "373 373 224 45 1.50 4.13 2554 714 12.34 0 0 0.00 0 0 5.00 0 0"
echo "226 223 338 58 2.00 2.69 1820 965 0.00 0 0 0.00 0 0 0.00 0 0"
echo "189 187 412 67 1.80 2.45 1950 823 8.50 2 1 15.00 1 0 22.00 0 1"
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 {
t.Fatal(err)
}
gm := &GPUManager{
GpuDataMap: make(map[string]*system.GPUData),
}
// Run the collector once; it should read four samples but skip the first and return
if err := gm.collectIntelStats(); err != nil {
t.Fatalf("collectIntelStats error: %v", err)
}
gpu := gm.GpuDataMap["0"]
require.NotNil(t, gpu)
// Power should be sum of samples 2-4 (first is skipped): 2.0 + 1.8 + 2.2 = 6.0
assert.EqualValues(t, 6.0, gpu.Power)
assert.InDelta(t, 8.26, gpu.PowerPkg, 0.01) // Allow small floating point differences
// Engines aggregated from samples 2-4
assert.EqualValues(t, 14.25, gpu.Engines["Render/3D"]) // 0.00 + 8.50 + 5.75
assert.EqualValues(t, 34.0, gpu.Engines["Video"]) // 0.00 + 22.00 + 12.00
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)
})
}
}

View File

@@ -1,25 +1,163 @@
package agent
import (
"fmt"
"log/slog"
"path"
"strings"
"time"
"github.com/henrygd/beszel/agent/deltatracker"
"github.com/henrygd/beszel/internal/entities/system"
psutilNet "github.com/shirou/gopsutil/v4/net"
)
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) {
// network stats
if len(a.netInterfaces) == 0 {
// if no network interfaces, initialize again
// this is a fix if agent started before network is online (#466)
// maybe refactor this in the future to not cache interface names at all so we
// don't miss an interface that's been added after agent started in any circumstance
a.initializeNetIoStats()
}
if systemStats.NetworkInterfaces == nil {
systemStats.NetworkInterfaces = make(map[string][4]uint64, 0)
}
if netIO, err := psutilNet.IOCounters(true); err == nil {
msElapsed := uint64(time.Since(a.netIoStats.Time).Milliseconds())
a.netIoStats.Time = time.Now()
totalBytesSent := uint64(0)
totalBytesRecv := uint64(0)
netInterfaceDeltaTracker.Cycle()
// sum all bytes sent and received
for _, v := range netIO {
// skip if not in valid network interfaces list
if _, exists := a.netInterfaces[v.Name]; !exists {
continue
}
totalBytesSent += v.BytesSent
totalBytesRecv += v.BytesRecv
// track deltas for each network interface
var upDelta, downDelta uint64
upKey, downKey := fmt.Sprintf("%sup", v.Name), fmt.Sprintf("%sdown", v.Name)
netInterfaceDeltaTracker.Set(upKey, v.BytesSent)
netInterfaceDeltaTracker.Set(downKey, v.BytesRecv)
if msElapsed > 0 {
upDelta = netInterfaceDeltaTracker.Delta(upKey) * 1000 / msElapsed
downDelta = netInterfaceDeltaTracker.Delta(downKey) * 1000 / msElapsed
}
// add interface to systemStats
systemStats.NetworkInterfaces[v.Name] = [4]uint64{upDelta, downDelta, v.BytesSent, v.BytesRecv}
}
// add to systemStats
var bytesSentPerSecond, bytesRecvPerSecond uint64
if msElapsed > 0 {
bytesSentPerSecond = (totalBytesSent - a.netIoStats.BytesSent) * 1000 / msElapsed
bytesRecvPerSecond = (totalBytesRecv - a.netIoStats.BytesRecv) * 1000 / msElapsed
}
networkSentPs := bytesToMegabytes(float64(bytesSentPerSecond))
networkRecvPs := bytesToMegabytes(float64(bytesRecvPerSecond))
// add check for issue (#150) where sent is a massive number
if networkSentPs > 10_000 || networkRecvPs > 10_000 {
slog.Warn("Invalid net stats. Resetting.", "sent", networkSentPs, "recv", networkRecvPs)
for _, v := range netIO {
if _, exists := a.netInterfaces[v.Name]; !exists {
continue
}
slog.Info(v.Name, "recv", v.BytesRecv, "sent", v.BytesSent)
}
// reset network I/O stats
a.initializeNetIoStats()
} else {
systemStats.NetworkSent = networkSentPs
systemStats.NetworkRecv = networkRecvPs
systemStats.Bandwidth[0], systemStats.Bandwidth[1] = bytesSentPerSecond, bytesRecvPerSecond
// update netIoStats
a.netIoStats.BytesSent = totalBytesSent
a.netIoStats.BytesRecv = totalBytesRecv
}
}
}
func (a *Agent) initializeNetIoStats() {
// reset valid network interfaces
a.netInterfaces = make(map[string]struct{}, 0)
// map of network interface names passed in via NICS env var
var nicsMap map[string]struct{}
nics, nicsEnvExists := GetEnv("NICS")
// parse NICS env var for whitelist / blacklist
nicsEnvVal, nicsEnvExists := GetEnv("NICS")
var nicCfg *NicConfig
if nicsEnvExists {
nicsMap = make(map[string]struct{}, 0)
for nic := range strings.SplitSeq(nics, ",") {
nicsMap[nic] = struct{}{}
}
nicCfg = newNicConfig(nicsEnvVal)
}
// reset network I/O stats
@@ -30,17 +168,11 @@ func (a *Agent) initializeNetIoStats() {
if netIO, err := psutilNet.IOCounters(true); err == nil {
a.netIoStats.Time = time.Now()
for _, v := range netIO {
switch {
// skip if nics exists and the interface is not in the list
case nicsEnvExists:
if _, nameInNics := nicsMap[v.Name]; !nameInNics {
continue
}
// otherwise run the interface name through the skipNetworkInterface function
default:
if a.skipNetworkInterface(v) {
continue
}
if nicsEnvExists && !isValidNic(v.Name, nicCfg) {
continue
}
if a.skipNetworkInterface(v) {
continue
}
slog.Info("Detected network interface", "name", v.Name, "sent", v.BytesSent, "recv", v.BytesRecv)
a.netIoStats.BytesSent += v.BytesSent
@@ -58,6 +190,7 @@ func (a *Agent) skipNetworkInterface(v psutilNet.IOCountersStat) bool {
strings.HasPrefix(v.Name, "br-"),
strings.HasPrefix(v.Name, "veth"),
strings.HasPrefix(v.Name, "bond"),
strings.HasPrefix(v.Name, "cali"),
v.BytesRecv == 0,
v.BytesSent == 0:
return true

259
agent/network_test.go Normal file
View 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)
})
}
}

View File

@@ -18,7 +18,6 @@ import (
"github.com/shirou/gopsutil/v4/host"
"github.com/shirou/gopsutil/v4/load"
"github.com/shirou/gopsutil/v4/mem"
psutilNet "github.com/shirou/gopsutil/v4/net"
)
// Sets initial / non-changing values about the host system
@@ -32,7 +31,7 @@ func (a *Agent) initializeSystemInfo() {
a.systemInfo.KernelVersion = version
a.systemInfo.Os = system.Darwin
} else if strings.Contains(platform, "indows") {
a.systemInfo.KernelVersion = strings.Replace(platform, "Microsoft ", "", 1) + " " + version
a.systemInfo.KernelVersion = fmt.Sprintf("%s %s", strings.Replace(platform, "Microsoft ", "", 1), version)
a.systemInfo.Os = system.Windows
} else if platform == "freebsd" {
a.systemInfo.Os = system.Freebsd
@@ -70,7 +69,7 @@ func (a *Agent) initializeSystemInfo() {
// Returns current info, stats about the host system
func (a *Agent) getSystemStats() system.Stats {
systemStats := system.Stats{}
var systemStats system.Stats
// battery
if battery.HasReadableBattery() {
@@ -101,14 +100,22 @@ func (a *Agent) getSystemStats() system.Stats {
systemStats.Swap = bytesToGigabytes(v.SwapTotal)
systemStats.SwapUsed = bytesToGigabytes(v.SwapTotal - v.SwapFree - v.SwapCached)
// cache + buffers value for default mem calculation
cacheBuff := v.Total - v.Free - v.Used
// htop memory calculation overrides
// note: gopsutil automatically adds SReclaimable to v.Cached
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" {
// 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.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
if a.zfs {
if arcSize, _ := getARCSize(); arcSize > 0 && arcSize < v.Used {
@@ -173,55 +180,7 @@ func (a *Agent) getSystemStats() system.Stats {
}
// network stats
if len(a.netInterfaces) == 0 {
// if no network interfaces, initialize again
// this is a fix if agent started before network is online (#466)
// maybe refactor this in the future to not cache interface names at all so we
// don't miss an interface that's been added after agent started in any circumstance
a.initializeNetIoStats()
}
if netIO, err := psutilNet.IOCounters(true); err == nil {
msElapsed := uint64(time.Since(a.netIoStats.Time).Milliseconds())
a.netIoStats.Time = time.Now()
totalBytesSent := uint64(0)
totalBytesRecv := uint64(0)
// sum all bytes sent and received
for _, v := range netIO {
// skip if not in valid network interfaces list
if _, exists := a.netInterfaces[v.Name]; !exists {
continue
}
totalBytesSent += v.BytesSent
totalBytesRecv += v.BytesRecv
}
// add to systemStats
var bytesSentPerSecond, bytesRecvPerSecond uint64
if msElapsed > 0 {
bytesSentPerSecond = (totalBytesSent - a.netIoStats.BytesSent) * 1000 / msElapsed
bytesRecvPerSecond = (totalBytesRecv - a.netIoStats.BytesRecv) * 1000 / msElapsed
}
networkSentPs := bytesToMegabytes(float64(bytesSentPerSecond))
networkRecvPs := bytesToMegabytes(float64(bytesRecvPerSecond))
// add check for issue (#150) where sent is a massive number
if networkSentPs > 10_000 || networkRecvPs > 10_000 {
slog.Warn("Invalid net stats. Resetting.", "sent", networkSentPs, "recv", networkRecvPs)
for _, v := range netIO {
if _, exists := a.netInterfaces[v.Name]; !exists {
continue
}
slog.Info(v.Name, "recv", v.BytesRecv, "sent", v.BytesSent)
}
// reset network I/O stats
a.initializeNetIoStats()
} else {
systemStats.NetworkSent = networkSentPs
systemStats.NetworkRecv = networkRecvPs
systemStats.Bandwidth[0], systemStats.Bandwidth[1] = bytesSentPerSecond, bytesRecvPerSecond
// update netIoStats
a.netIoStats.BytesSent = totalBytesSent
a.netIoStats.BytesRecv = totalBytesRecv
}
}
a.updateNetworkStats(&systemStats)
// temperatures
// TODO: maybe refactor to methods on systemStats
@@ -261,6 +220,7 @@ func (a *Agent) getSystemStats() system.Stats {
}
// update base system info
a.systemInfo.ConnectionType = a.connectionManager.ConnectionType
a.systemInfo.Cpu = systemStats.Cpu
a.systemInfo.LoadAvg = systemStats.LoadAvg
// TODO: remove these in future release in favor of load avg array

107
agent/systemd.go Normal file
View File

@@ -0,0 +1,107 @@
//go:build linux
package agent
import (
"context"
"log/slog"
"math"
"strings"
"sync"
"github.com/coreos/go-systemd/v22/dbus"
"github.com/henrygd/beszel/internal/entities/systemd"
)
// systemdManager manages the collection of systemd service statistics.
type systemdManager struct {
conn *dbus.Conn
serviceStatsMap map[string]*systemd.Service
mu sync.Mutex
}
// newSystemdManager creates a new systemdManager.
func newSystemdManager() (*systemdManager, error) {
conn, err := dbus.New()
if err != nil {
if strings.Contains(err.Error(), "permission denied") {
slog.Error("Permission denied when connecting to systemd. Run as root or with appropriate user permissions.", "err", err)
return nil, err
}
slog.Error("Error connecting to systemd", "err", err)
return nil, err
}
return &systemdManager{
conn: conn,
serviceStatsMap: make(map[string]*systemd.Service),
}, nil
}
// getServiceStats collects statistics for all running systemd services.
func (sm *systemdManager) getServiceStats() []*systemd.Service {
units, err := sm.conn.ListUnitsContext(context.Background())
if err != nil {
slog.Error("Error listing systemd units", "err", err)
return nil
}
var services []*systemd.Service
for _, unit := range units {
if strings.HasSuffix(unit.Name, ".service") {
service := sm.updateServiceStats(unit)
services = append(services, service)
}
}
return services
}
// updateServiceStats updates the statistics for a single systemd service.
func (sm *systemdManager) updateServiceStats(unit dbus.UnitStatus) *systemd.Service {
sm.mu.Lock()
defer sm.mu.Unlock()
props, err := sm.conn.GetUnitTypeProperties(unit.Name, "Service")
if err != nil {
slog.Debug("could not get unit type properties", "unit", unit.Name, "err", err)
return &systemd.Service{
Name: unit.Name,
Status: unit.ActiveState,
}
}
var cpuUsage uint64
if val, ok := props["CPUUsageNSec"]; ok {
if v, ok := val.(uint64); ok {
cpuUsage = v
}
}
var memUsage uint64
if val, ok := props["MemoryCurrent"]; ok {
if v, ok := val.(uint64); ok {
memUsage = v
}
}
service, exists := sm.serviceStatsMap[unit.Name]
if !exists {
service = &systemd.Service{
Name: unit.Name,
Status: unit.ActiveState,
}
sm.serviceStatsMap[unit.Name] = service
}
service.Status = unit.ActiveState
// If memUsage is MaxUint64 the api is saying it's not available, return 0
if memUsage == math.MaxUint64 {
memUsage = 0
}
service.Mem = float64(memUsage) / (1024 * 1024) // Convert to MB
service.CalculateCPUPercent(cpuUsage)
return service
}

View File

@@ -0,0 +1,18 @@
//go:build !linux
package agent
import "github.com/henrygd/beszel/internal/entities/systemd"
// systemdManager manages the collection of systemd service statistics.
type systemdManager struct{}
// newSystemdManager creates a new systemdManager.
func newSystemdManager() (*systemdManager, error) {
return &systemdManager{}, nil
}
// getServiceStats returns nil for non-linux systems.
func (sm *systemdManager) getServiceStats() []*systemd.Service {
return nil
}

View File

@@ -30,11 +30,11 @@ func (s *systemdRestarter) Restart() error {
type openRCRestarter struct{ cmd string }
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
}
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 }

View File

@@ -6,7 +6,7 @@ import "github.com/blang/semver"
const (
// Version is the current version of the application.
Version = "0.12.7"
Version = "0.12.12"
// AppName is the name of the application.
AppName = "beszel"
)

40
go.mod
View File

@@ -3,25 +3,26 @@ module github.com/henrygd/beszel
go 1.25.1
// lock shoutrrr to specific version to allow review before updating
replace github.com/nicholas-fedor/shoutrrr => github.com/nicholas-fedor/shoutrrr v0.8.8
replace github.com/nicholas-fedor/shoutrrr => github.com/nicholas-fedor/shoutrrr v0.9.1
require (
github.com/blang/semver v3.5.1+incompatible
github.com/coreos/go-systemd/v22 v22.6.0
github.com/distatus/battery v0.11.0
github.com/fxamacker/cbor/v2 v2.9.0
github.com/gliderlabs/ssh v0.3.8
github.com/google/uuid v1.6.0
github.com/lxzan/gws v1.8.9
github.com/nicholas-fedor/shoutrrr v0.8.17
github.com/nicholas-fedor/shoutrrr v0.9.1
github.com/pocketbase/dbx v1.11.0
github.com/pocketbase/pocketbase v0.29.3
github.com/shirou/gopsutil/v4 v4.25.6
github.com/spf13/cast v1.9.2
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.7
github.com/stretchr/testify v1.11.0
golang.org/x/crypto v0.41.0
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b
github.com/pocketbase/pocketbase v0.30.0
github.com/shirou/gopsutil/v4 v4.25.8
github.com/spf13/cast v1.10.0
github.com/spf13/cobra v1.10.1
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
golang.org/x/crypto v0.42.0
golang.org/x/exp v0.0.0-20250911091902-df9299821621
gopkg.in/yaml.v3 v3.0.1
)
@@ -33,17 +34,18 @@ require (
github.com/dolthub/maphash v0.1.0 // indirect
github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/ebitengine/purego v0.9.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/ganigeorgiev/fexpr v0.5.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/go-sql-driver/mysql v1.9.1 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/lufia/plan9stats v0.0.0-20250821153705-5981dea3221d // indirect
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
@@ -54,12 +56,12 @@ require (
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/image v0.30.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/image v0.31.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/oauth2 v0.31.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
howett.net/plist v1.0.1 // indirect
modernc.org/libc v1.66.3 // indirect

132
go.sum
View File

@@ -1,5 +1,7 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
@@ -7,6 +9,8 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -21,22 +25,22 @@ github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCO
github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/ganigeorgiev/fexpr v0.5.0 h1:XA9JxtTE/Xm+g/JFI6RfZEHSiQlk+1glLvRK1Lpv/Tk=
github.com/ganigeorgiev/fexpr v0.5.0/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
@@ -47,19 +51,21 @@ github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtS
github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY=
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jarcoal/httpmock v1.4.0 h1:BvhqnH0JAYbNudL2GMJKgOHe2CtKlzJ/5rWKyp+hc2k=
github.com/jarcoal/httpmock v1.4.0/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A=
github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
@@ -67,8 +73,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lufia/plan9stats v0.0.0-20250821153705-5981dea3221d h1:vFzYZc8yji+9DmNRhpEbs8VBK4CgV/DPfGzeVJSSp/8=
github.com/lufia/plan9stats v0.0.0-20250821153705-5981dea3221d/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/lxzan/gws v1.8.9 h1:VU3SGUeWlQrEwfUSfokcZep8mdg/BrUF+y73YYshdBM=
github.com/lxzan/gws v1.8.9/go.mod h1:d9yHaR1eDTBHagQC6KY7ycUOaz5KWeqQtP3xu7aMK8Y=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
@@ -77,19 +83,19 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nicholas-fedor/shoutrrr v0.8.8 h1:F/oyoatWK5cbHPPgkjRZrA0262TP7KWuUQz9KskRtR8=
github.com/nicholas-fedor/shoutrrr v0.8.8/go.mod h1:T30Y+eoZFEjDk4HtOItcHQioZSOe3Z6a6aNfSz6jc5c=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/nicholas-fedor/shoutrrr v0.9.1 h1:SEBhM6P1favzILO0f55CY3P9JwvM9RZ7B1ZMCl+Injs=
github.com/nicholas-fedor/shoutrrr v0.9.1/go.mod h1:khue5m8LYyMzdPWuJxDTJeT89l9gjwjA+a+r0e8qxxk=
github.com/onsi/ginkgo/v2 v2.25.3 h1:Ty8+Yi/ayDAGtk4XxmmfUy4GabvM+MegeB4cDLRi6nw=
github.com/onsi/ginkgo/v2 v2.25.3/go.mod h1:43uiyQC4Ed2tkOzLsEYm7hnrb7UJTWHYNsuy3bG/snE=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU=
github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
github.com/pocketbase/pocketbase v0.29.3 h1:Mj8o5awsbVJIdIoTuQNhfC2oL/c4aImQ3RyfFZlzFVg=
github.com/pocketbase/pocketbase v0.29.3/go.mod h1:oGpT67LObxCFK4V2fSL7J9YnPbBnnshOpJ5v3zcneww=
github.com/pocketbase/pocketbase v0.30.0 h1:7v9O3hBYyHyptnnFjdP8tEJIuyHEfjhG6PC4gjf5eoE=
github.com/pocketbase/pocketbase v0.30.0/go.mod h1:gZIwampw4VqMcEdGHwBZgSa54xWIDgVJb4uINUMXLmA=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
@@ -97,19 +103,19 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/shirou/gopsutil/v4 v4.25.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970=
github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
@@ -120,42 +126,44 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU=
golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/image v0.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA=
golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -165,18 +173,20 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/cc/v4 v4.26.4 h1:jPhG8oNjtTYuP2FA4YefTJ/wioNUGALmGuEWt7SUR6s=
modernc.org/cc/v4 v4.26.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
modernc.org/fileutil v1.3.28 h1:Vp156KUA2nPu9F1NEv036x9UGOjg2qsi5QlWTjZmtMk=
modernc.org/fileutil v1.3.28/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
modernc.org/libc v1.66.9 h1:YkHp7E1EWrN2iyNav7JE/nHasmshPvlGkon1VxGqOw0=
modernc.org/libc v1.66.9/go.mod h1:aVdcY7udcawRqauu0HukYYxtBSizV+R80n/6aQe9D5k=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=

View File

@@ -25,7 +25,12 @@ type alertInfo struct {
// startWorker is a long-running goroutine that processes alert tasks
// every x seconds. It must be running to process status alerts.
func (am *AlertManager) startWorker() {
tick := time.Tick(15 * time.Second)
processPendingAlerts := time.Tick(15 * time.Second)
// check for status alerts that are not resolved when system comes up
// (can be removed if we figure out core bug in #1052)
checkStatusAlerts := time.Tick(561 * time.Second)
for {
select {
case <-am.stopChan:
@@ -41,7 +46,9 @@ func (am *AlertManager) startWorker() {
case "cancel":
am.pendingAlerts.Delete(task.alertRecord.Id)
}
case <-tick:
case <-checkStatusAlerts:
resolveStatusAlerts(am.hub)
case <-processPendingAlerts:
// Check for expired alerts every tick
now := time.Now()
for key, value := range am.pendingAlerts.Range {
@@ -170,3 +177,35 @@ func (am *AlertManager) sendStatusAlert(alertStatus string, systemName string, a
LinkText: "View " + systemName,
})
}
// resolveStatusAlerts resolves any status alerts that weren't resolved
// when system came up (https://github.com/henrygd/beszel/issues/1052)
func resolveStatusAlerts(app core.App) error {
db := app.DB()
// Find all active status alerts where the system is actually up
var alertIds []string
err := db.NewQuery(`
SELECT a.id
FROM alerts a
JOIN systems s ON a.system = s.id
WHERE a.name = 'Status'
AND a.triggered = true
AND s.status = 'up'
`).Column(&alertIds)
if err != nil {
return err
}
// resolve all matching alert records
for _, alertId := range alertIds {
alert, err := app.FindRecordById("alerts", alertId)
if err != nil {
return err
}
alert.Set("triggered", false)
err = app.Save(alert)
if err != nil {
return err
}
}
return nil
}

View File

@@ -13,6 +13,7 @@ import (
"testing/synctest"
"time"
"github.com/henrygd/beszel/internal/alerts"
beszelTests "github.com/henrygd/beszel/internal/tests"
"github.com/pocketbase/dbx"
@@ -369,33 +370,9 @@ func TestUserAlertsApi(t *testing.T) {
}
}
func getHubWithUser(t *testing.T) (*beszelTests.TestHub, *core.Record) {
hub, err := beszelTests.NewTestHub(t.TempDir())
assert.NoError(t, err)
hub.StartHub()
// Manually initialize the system manager to bind event hooks
err = hub.GetSystemManager().Initialize()
assert.NoError(t, err)
// Create a test user
user, err := beszelTests.CreateUser(hub, "test@example.com", "password")
assert.NoError(t, err)
// Create user settings for the test user (required for alert notifications)
userSettingsData := map[string]any{
"user": user.Id,
"settings": `{"emails":[test@example.com],"webhooks":[]}`,
}
_, err = beszelTests.CreateRecord(hub, "user_settings", userSettingsData)
assert.NoError(t, err)
return hub, user
}
func TestStatusAlerts(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
hub, user := getHubWithUser(t)
hub, user := beszelTests.GetHubWithUser(t)
defer hub.Cleanup()
systems, err := beszelTests.CreateSystems(hub, 4, user.Id, "paused")
@@ -476,7 +453,7 @@ func TestStatusAlerts(t *testing.T) {
func TestAlertsHistory(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
hub, user := getHubWithUser(t)
hub, user := beszelTests.GetHubWithUser(t)
defer hub.Cleanup()
// Create systems and alerts
@@ -602,3 +579,102 @@ func TestAlertsHistory(t *testing.T) {
assert.EqualValues(t, 2, totalHistoryCount, "Should have 2 total alert history records")
})
}
func TestResolveStatusAlerts(t *testing.T) {
hub, user := beszelTests.GetHubWithUser(t)
defer hub.Cleanup()
// Create a systemUp
systemUp, err := beszelTests.CreateRecord(hub, "systems", map[string]any{
"name": "test-system",
"users": []string{user.Id},
"host": "127.0.0.1",
"status": "up",
})
assert.NoError(t, err)
systemDown, err := beszelTests.CreateRecord(hub, "systems", map[string]any{
"name": "test-system-2",
"users": []string{user.Id},
"host": "127.0.0.2",
"status": "up",
})
assert.NoError(t, err)
// Create a status alertUp for the system
alertUp, err := beszelTests.CreateRecord(hub, "alerts", map[string]any{
"name": "Status",
"system": systemUp.Id,
"user": user.Id,
"min": 1,
})
assert.NoError(t, err)
alertDown, err := beszelTests.CreateRecord(hub, "alerts", map[string]any{
"name": "Status",
"system": systemDown.Id,
"user": user.Id,
"min": 1,
})
assert.NoError(t, err)
// Verify alert is not triggered initially
assert.False(t, alertUp.GetBool("triggered"), "Alert should not be triggered initially")
// Set the system to 'up' (this should not trigger the alert)
systemUp.Set("status", "up")
err = hub.SaveNoValidate(systemUp)
assert.NoError(t, err)
systemDown.Set("status", "down")
err = hub.SaveNoValidate(systemDown)
assert.NoError(t, err)
// Wait a moment for any processing
time.Sleep(10 * time.Millisecond)
// Verify alertUp is still not triggered after setting system to up
alertUp, err = hub.FindFirstRecordByFilter("alerts", "id={:id}", dbx.Params{"id": alertUp.Id})
assert.NoError(t, err)
assert.False(t, alertUp.GetBool("triggered"), "Alert should not be triggered when system is up")
// Manually set both alerts triggered to true
alertUp.Set("triggered", true)
err = hub.SaveNoValidate(alertUp)
assert.NoError(t, err)
alertDown.Set("triggered", true)
err = hub.SaveNoValidate(alertDown)
assert.NoError(t, err)
// Verify we have exactly one alert with triggered true
triggeredCount, err := hub.CountRecords("alerts", dbx.HashExp{"triggered": true})
assert.NoError(t, err)
assert.EqualValues(t, 2, triggeredCount, "Should have exactly two alerts with triggered true")
// Verify the specific alertUp is triggered
alertUp, err = hub.FindFirstRecordByFilter("alerts", "id={:id}", dbx.Params{"id": alertUp.Id})
assert.NoError(t, err)
assert.True(t, alertUp.GetBool("triggered"), "Alert should be triggered")
// Verify we have two unresolved alert history records
alertHistoryCount, err := hub.CountRecords("alerts_history", dbx.HashExp{"resolved": ""})
assert.NoError(t, err)
assert.EqualValues(t, 2, alertHistoryCount, "Should have exactly two unresolved alert history records")
err = alerts.ResolveStatusAlerts(hub)
assert.NoError(t, err)
// Verify alertUp is not triggered after resolving
alertUp, err = hub.FindFirstRecordByFilter("alerts", "id={:id}", dbx.Params{"id": alertUp.Id})
assert.NoError(t, err)
assert.False(t, alertUp.GetBool("triggered"), "Alert should not be triggered after resolving")
// Verify alertDown is still triggered
alertDown, err = hub.FindFirstRecordByFilter("alerts", "id={:id}", dbx.Params{"id": alertDown.Id})
assert.NoError(t, err)
assert.True(t, alertDown.GetBool("triggered"), "Alert should still be triggered after resolving")
// Verify we have one unresolved alert history record
alertHistoryCount, err = hub.CountRecords("alerts_history", dbx.HashExp{"resolved": ""})
assert.NoError(t, err)
assert.EqualValues(t, 1, alertHistoryCount, "Should have exactly one unresolved alert history record")
}

View File

@@ -1,3 +1,6 @@
//go:build testing
// +build testing
package alerts
import (
@@ -53,3 +56,7 @@ func (am *AlertManager) ForceExpirePendingAlerts() {
return true
})
}
func ResolveStatusAlerts(app core.App) error {
return resolveStatusAlerts(app)
}

View File

@@ -0,0 +1,25 @@
FROM --platform=$BUILDPLATFORM golang:alpine AS builder
WORKDIR /app
COPY ../go.mod ../go.sum ./
RUN go mod download
# Copy source files
COPY . ./
# Build
ARG TARGETOS TARGETARCH
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./internal/cmd/agent
# --------------------------
# Final image
# Note: must cap_add: [CAP_PERFMON] and mount /dev/dri/ as volume
# --------------------------
FROM alpine:edge
COPY --from=builder /agent /agent
RUN apk add --no-cache -X https://dl-cdn.alpinelinux.org/alpine/edge/testing igt-gpu-tools
ENTRYPOINT ["/agent"]

View File

@@ -2,15 +2,18 @@ FROM --platform=$BUILDPLATFORM golang:alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
# RUN go mod download
COPY *.go ./
COPY cmd ./cmd
COPY internal ./internal
COPY ../go.mod ../go.sum ./
RUN go mod download
# Copy source files
COPY . ./
# Build
ARG TARGETOS TARGETARCH
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./cmd/agent
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./internal/cmd/agent
RUN rm -rf /tmp/*
# --------------------------
# Final image: GPU-enabled agent with nvidia-smi
@@ -18,4 +21,7 @@ RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-
FROM nvidia/cuda:12.2.2-base-ubuntu22.04
COPY --from=builder /agent /agent
# this is so we don't need to create the /tmp directory in the scratch container
COPY --from=builder /tmp /tmp
ENTRYPOINT ["/agent"]

View File

@@ -6,6 +6,7 @@ import (
"time"
"github.com/henrygd/beszel/internal/entities/container"
"github.com/henrygd/beszel/internal/entities/systemd"
)
type Stats struct {
@@ -38,19 +39,22 @@ type Stats struct {
Bandwidth [2]uint64 `json:"b,omitzero" cbor:"26,keyasint,omitzero"` // [sent bytes, recv bytes]
MaxBandwidth [2]uint64 `json:"bm,omitzero" cbor:"27,keyasint,omitzero"` // [sent bytes, recv bytes]
// TODO: remove other load fields in future release in favor of load avg array
LoadAvg [3]float64 `json:"la,omitempty" cbor:"28,keyasint"`
Battery [2]uint8 `json:"bat,omitzero" cbor:"29,keyasint,omitzero"` // [percent, charge state, current]
MaxMem float64 `json:"mm,omitempty" cbor:"30,keyasint,omitempty"`
LoadAvg [3]float64 `json:"la,omitempty" cbor:"28,keyasint"`
Battery [2]uint8 `json:"bat,omitzero" cbor:"29,keyasint,omitzero"` // [percent, charge state, current]
MaxMem float64 `json:"mm,omitempty" cbor:"30,keyasint,omitempty"`
NetworkInterfaces map[string][4]uint64 `json:"ni,omitempty" cbor:"31,keyasint,omitempty"` // [upload bytes, download bytes, total upload, total download]
}
type GPUData struct {
Name string `json:"n" cbor:"0,keyasint"`
Temperature float64 `json:"-"`
MemoryUsed float64 `json:"mu,omitempty" cbor:"1,keyasint,omitempty"`
MemoryTotal float64 `json:"mt,omitempty" cbor:"2,keyasint,omitempty"`
Usage float64 `json:"u" cbor:"3,keyasint"`
Power float64 `json:"p,omitempty" cbor:"4,keyasint,omitempty"`
Count float64 `json:"-"`
Name string `json:"n" cbor:"0,keyasint"`
Temperature float64 `json:"-"`
MemoryUsed float64 `json:"mu,omitempty,omitzero" cbor:"1,keyasint,omitempty,omitzero"`
MemoryTotal float64 `json:"mt,omitempty,omitzero" cbor:"2,keyasint,omitempty,omitzero"`
Usage float64 `json:"u" cbor:"3,keyasint,omitempty"`
Power float64 `json:"p,omitempty" cbor:"4,keyasint,omitempty"`
Count float64 `json:"-"`
Engines map[string]float64 `json:"e,omitempty" cbor:"5,keyasint,omitempty"`
PowerPkg float64 `json:"pp,omitempty" cbor:"6,keyasint,omitempty"`
}
type FsStats struct {
@@ -83,6 +87,14 @@ const (
Freebsd
)
type ConnectionType = uint8
const (
ConnectionTypeNone ConnectionType = iota
ConnectionTypeSSH
ConnectionTypeWebSocket
)
type Info struct {
Hostname string `json:"h" cbor:"0,keyasint"`
KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"`
@@ -104,12 +116,14 @@ type Info struct {
LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"`
BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"`
// TODO: remove load fields in future release in favor of load avg array
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"`
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"`
ConnectionType ConnectionType `json:"ct,omitempty" cbor:"20,keyasint,omitempty,omitzero"`
}
// Final data structure to return to the hub
type CombinedData struct {
Stats Stats `json:"stats" cbor:"0,keyasint"`
Info Info `json:"info" cbor:"1,keyasint"`
Containers []*container.Stats `json:"container" cbor:"2,keyasint"`
Stats Stats `json:"stats" cbor:"0,keyasint"`
Info Info `json:"info" cbor:"1,keyasint"`
Containers []*container.Stats `json:"container" cbor:"2,keyasint"`
SystemdServices []*systemd.Service `json:"systemd,omitempty" cbor:"3,keyasint,omitempty"`
}

View File

@@ -0,0 +1,34 @@
package systemd
import (
"runtime"
"time"
)
// Service represents a single systemd service with its stats.
type Service struct {
Name string `json:"n" cbor:"0,keyasint"`
Status string `json:"s" cbor:"1,keyasint"`
Cpu float64 `json:"c" cbor:"2,keyasint"`
Mem float64 `json:"m" cbor:"3,keyasint"`
PrevCpuUsage uint64 `json:"-"`
PrevReadTime time.Time `json:"-"`
}
// CalculateCPUPercent calculates the CPU usage percentage for the service.
func (s *Service) CalculateCPUPercent(cpuUsage uint64) {
if s.PrevReadTime.IsZero() {
s.Cpu = 0
} else {
duration := time.Since(s.PrevReadTime).Nanoseconds()
if duration > 0 {
coreCount := int64(runtime.NumCPU())
duration *= coreCount
cpuPercent := float64(cpuUsage-s.PrevCpuUsage) / float64(duration)
s.Cpu = cpuPercent * 100
}
}
s.PrevCpuUsage = cpuUsage
s.PrevReadTime = time.Now()
}

View File

@@ -161,6 +161,20 @@ func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error
return nil, err
}
}
// add new systemd_stats record
if len(data.SystemdServices) > 0 {
systemdStatsCollection, err := hub.FindCachedCollectionByNameOrId("systemd_stats")
if err != nil {
return nil, err
}
systemdStatsRecord := core.NewRecord(systemdStatsCollection)
systemdStatsRecord.Set("system", systemRecord.Id)
systemdStatsRecord.Set("stats", data.SystemdServices)
systemdStatsRecord.Set("type", "1m")
if err := hub.SaveNoValidate(systemdStatsRecord); err != nil {
return nil, err
}
}
// update system record (do this last because it triggers alerts and we need above records to be inserted first)
systemRecord.Set("status", up)

View File

@@ -22,6 +22,12 @@ func Update(cmd *cobra.Command, _ []string) {
// Check if china-mirrors flag is set
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{
ArchiveExecutable: "beszel",
DataDir: dataDir,
@@ -35,11 +41,8 @@ func Update(cmd *cobra.Command, _ []string) {
}
// make sure the file is executable
exePath, err := os.Executable()
if err == nil {
if err := os.Chmod(exePath, 0755); 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

View File

@@ -520,6 +520,96 @@ func init() {
],
"system": false
},
{
"id": "systemd_stats_collection",
"listRule": "@request.auth.id != \"\"",
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"name": "systemd_stats",
"type": "base",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"cascadeDelete": true,
"collectionId": "2hz5ncl8tizk5nx",
"hidden": false,
"id": "hutcu6ps",
"maxSelect": 1,
"minSelect": 0,
"name": "system",
"presentable": false,
"required": true,
"system": false,
"type": "relation"
},
{
"hidden": false,
"id": "r39hhnil",
"maxSize": 2000000,
"name": "stats",
"presentable": false,
"required": true,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "vo7iuj96",
"maxSelect": 1,
"name": "type",
"presentable": false,
"required": true,
"system": false,
"type": "select",
"values": [
"1m",
"10m",
"20m",
"120m",
"480m"
]
},
{
"hidden": false,
"id": "autodate2990389176",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": false,
"type": "autodate"
},
{
"hidden": false,
"id": "autodate3332085495",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}
],
"indexes": [
"CREATE INDEX ` + "`" + `idx_systemd_stats` + "`" + ` ON ` + "`" + `systemd_stats` + "`" + ` (\n ` + "`" + `system` + "`" + `,\n ` + "`" + `type` + "`" + `,\n ` + "`" + `created` + "`" + `\n)"
],
"system": false
},
{
"id": "4afacsdnlu8q8r2",
"listRule": "@request.auth.id != \"\" && user.id = @request.auth.id",

View 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
})
}

View File

@@ -225,6 +225,19 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
sum.MaxBandwidth[0] = max(sum.MaxBandwidth[0], stats.MaxBandwidth[0], stats.Bandwidth[0])
sum.MaxBandwidth[1] = max(sum.MaxBandwidth[1], stats.MaxBandwidth[1], stats.Bandwidth[1])
// Accumulate network interfaces
if sum.NetworkInterfaces == nil {
sum.NetworkInterfaces = make(map[string][4]uint64, len(stats.NetworkInterfaces))
}
for key, value := range stats.NetworkInterfaces {
sum.NetworkInterfaces[key] = [4]uint64{
sum.NetworkInterfaces[key][0] + value[0],
sum.NetworkInterfaces[key][1] + value[1],
max(sum.NetworkInterfaces[key][2], value[2]),
max(sum.NetworkInterfaces[key][3], value[3]),
}
}
// Accumulate temperatures
if stats.Temperatures != nil {
if sum.Temperatures == nil {
@@ -271,6 +284,16 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
gpu.Usage += value.Usage
gpu.Power += value.Power
gpu.Count += value.Count
if value.Engines != nil {
if gpu.Engines == nil {
gpu.Engines = make(map[string]float64, len(value.Engines))
}
for engineKey, engineValue := range value.Engines {
gpu.Engines[engineKey] += engineValue
}
}
sum.GPUData[id] = gpu
}
}
@@ -299,6 +322,19 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
sum.Bandwidth[0] = sum.Bandwidth[0] / uint64(count)
sum.Bandwidth[1] = sum.Bandwidth[1] / uint64(count)
sum.Battery[0] = uint8(batterySum / int(count))
// Average network interfaces
if sum.NetworkInterfaces != nil {
for key := range sum.NetworkInterfaces {
sum.NetworkInterfaces[key] = [4]uint64{
sum.NetworkInterfaces[key][0] / uint64(count),
sum.NetworkInterfaces[key][1] / uint64(count),
sum.NetworkInterfaces[key][2],
sum.NetworkInterfaces[key][3],
}
}
}
// Average temperatures
if sum.Temperatures != nil && tempCount > 0 {
for key := range sum.Temperatures {
@@ -327,6 +363,13 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
gpu.Usage = twoDecimals(gpu.Usage / count)
gpu.Power = twoDecimals(gpu.Power / count)
gpu.Count = twoDecimals(gpu.Count / count)
if gpu.Engines != nil {
for engineKey := range gpu.Engines {
gpu.Engines[engineKey] = twoDecimals(gpu.Engines[engineKey] / count)
}
}
sum.GPUData[id] = gpu
}
}

View File

@@ -175,7 +175,7 @@ func TestDeleteOldSystemStats(t *testing.T) {
}
// Run deletion
err = records.TestDeleteOldSystemStats(hub)
err = records.DeleteOldSystemStats(hub)
require.NoError(t, err)
// Verify results
@@ -268,7 +268,7 @@ func TestDeleteOldAlertsHistory(t *testing.T) {
assert.Equal(t, int64(tc.alertCount), countBefore, "Initial count should match")
// Run deletion
err = records.TestDeleteOldAlertsHistory(hub, tc.countToKeep, tc.countBeforeDeletion)
err = records.DeleteOldAlertsHistory(hub, tc.countToKeep, tc.countBeforeDeletion)
require.NoError(t, err)
// Count after deletion
@@ -332,7 +332,7 @@ func TestDeleteOldAlertsHistoryEdgeCases(t *testing.T) {
}
// Should not error and should not delete anything
err = records.TestDeleteOldAlertsHistory(hub, 10, 20)
err = records.DeleteOldAlertsHistory(hub, 10, 20)
require.NoError(t, err)
count, err := hub.CountRecords("alerts_history")
@@ -346,7 +346,7 @@ func TestDeleteOldAlertsHistoryEdgeCases(t *testing.T) {
require.NoError(t, err)
// Should not error with empty table
err = records.TestDeleteOldAlertsHistory(hub, 10, 20)
err = records.DeleteOldAlertsHistory(hub, 10, 20)
require.NoError(t, err)
})
}
@@ -376,7 +376,7 @@ func TestTwoDecimals(t *testing.T) {
}
for _, tc := range testCases {
result := records.TestTwoDecimals(tc.input)
result := records.TwoDecimals(tc.input)
assert.InDelta(t, tc.expected, result, 0.02, "twoDecimals(%f) should equal %f", tc.input, tc.expected)
}
}

View File

@@ -7,17 +7,17 @@ import (
"github.com/pocketbase/pocketbase/core"
)
// TestDeleteOldSystemStats exposes deleteOldSystemStats for testing
func TestDeleteOldSystemStats(app core.App) error {
// DeleteOldSystemStats exposes deleteOldSystemStats for testing
func DeleteOldSystemStats(app core.App) error {
return deleteOldSystemStats(app)
}
// TestDeleteOldAlertsHistory exposes deleteOldAlertsHistory for testing
func TestDeleteOldAlertsHistory(app core.App, countToKeep, countBeforeDeletion int) error {
// DeleteOldAlertsHistory exposes deleteOldAlertsHistory for testing
func DeleteOldAlertsHistory(app core.App, countToKeep, countBeforeDeletion int) error {
return deleteOldAlertsHistory(app, countToKeep, countBeforeDeletion)
}
// TestTwoDecimals exposes twoDecimals for testing
func TestTwoDecimals(value float64) float64 {
// TwoDecimals exposes twoDecimals for testing
func TwoDecimals(value float64) float64 {
return twoDecimals(value)
}

View File

@@ -1,12 +1,12 @@
{
"name": "beszel",
"version": "0.12.7",
"version": "0.12.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "beszel",
"version": "0.12.7",
"version": "0.12.12",
"dependencies": {
"@henrygd/queue": "^1.0.7",
"@henrygd/semaphore": "^0.0.2",
@@ -46,6 +46,7 @@
"valibot": "^0.42.1"
},
"devDependencies": {
"@biomejs/biome": "2.2.3",
"@lingui/cli": "^5.4.1",
"@lingui/swc-plugin": "^5.6.1",
"@lingui/vite-plugin": "^5.4.1",
@@ -68,7 +69,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
@@ -82,7 +83,7 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.27.1",
@@ -97,7 +98,7 @@
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz",
"integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -107,7 +108,7 @@
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz",
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
@@ -138,7 +139,7 @@
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
"integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.3",
@@ -155,7 +156,7 @@
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
"integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.27.2",
@@ -172,7 +173,7 @@
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -182,7 +183,7 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.27.1",
@@ -196,7 +197,7 @@
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
"integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.27.1",
@@ -214,7 +215,7 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -224,7 +225,7 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -234,7 +235,7 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -244,7 +245,7 @@
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz",
"integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.27.2",
@@ -258,7 +259,7 @@
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz",
"integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.2"
@@ -286,7 +287,7 @@
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
@@ -301,7 +302,7 @@
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz",
"integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
@@ -320,7 +321,7 @@
"version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
"integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
@@ -330,6 +331,169 @@
"node": ">=6.9.0"
}
},
"node_modules/@biomejs/biome": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.2.3.tgz",
"integrity": "sha512-9w0uMTvPrIdvUrxazZ42Ib7t8Y2yoGLKLdNne93RLICmaHw7mcLv4PPb5LvZLJF3141gQHiCColOh/v6VWlWmg==",
"dev": true,
"license": "MIT OR Apache-2.0",
"bin": {
"biome": "bin/biome"
},
"engines": {
"node": ">=14.21.3"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/biome"
},
"optionalDependencies": {
"@biomejs/cli-darwin-arm64": "2.2.3",
"@biomejs/cli-darwin-x64": "2.2.3",
"@biomejs/cli-linux-arm64": "2.2.3",
"@biomejs/cli-linux-arm64-musl": "2.2.3",
"@biomejs/cli-linux-x64": "2.2.3",
"@biomejs/cli-linux-x64-musl": "2.2.3",
"@biomejs/cli-win32-arm64": "2.2.3",
"@biomejs/cli-win32-x64": "2.2.3"
}
},
"node_modules/@biomejs/cli-darwin-arm64": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.2.3.tgz",
"integrity": "sha512-OrqQVBpadB5eqzinXN4+Q6honBz+tTlKVCsbEuEpljK8ASSItzIRZUA02mTikl3H/1nO2BMPFiJ0nkEZNy3B1w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-darwin-x64": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.2.3.tgz",
"integrity": "sha512-OCdBpb1TmyfsTgBAM1kPMXyYKTohQ48WpiN9tkt9xvU6gKVKHY4oVwteBebiOqyfyzCNaSiuKIPjmHjUZ2ZNMg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-arm64": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.2.3.tgz",
"integrity": "sha512-g/Uta2DqYpECxG+vUmTAmUKlVhnGEcY7DXWgKP8ruLRa8Si1QHsWknPY3B/wCo0KgYiFIOAZ9hjsHfNb9L85+g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-arm64-musl": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.3.tgz",
"integrity": "sha512-q3w9jJ6JFPZPeqyvwwPeaiS/6NEszZ+pXKF+IczNo8Xj6fsii45a4gEEicKyKIytalV+s829ACZujQlXAiVLBQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-x64": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.2.3.tgz",
"integrity": "sha512-LEtyYL1fJsvw35CxrbQ0gZoxOG3oZsAjzfRdvRBRHxOpQ91Q5doRVjvWW/wepgSdgk5hlaNzfeqpyGmfSD0Eyw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-x64-musl": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.3.tgz",
"integrity": "sha512-y76Dn4vkP1sMRGPFlNc+OTETBhGPJ90jY3il6jAfur8XWrYBQV3swZ1Jo0R2g+JpOeeoA0cOwM7mJG6svDz79w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-win32-arm64": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.2.3.tgz",
"integrity": "sha512-Ms9zFYzjcJK7LV+AOMYnjN3pV3xL8Prxf9aWdDVL74onLn5kcvZ1ZMQswE5XHtnd/r/0bnUd928Rpbs14BzVmA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-win32-x64": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.2.3.tgz",
"integrity": "sha512-gvCpewE7mBwBIpqk1YrUqNR4mCiyJm6UI3YWQQXkedSSEwzRdodRpaKhbdbHw1/hmTWOVXQ+Eih5Qctf4TCVOQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz",
@@ -856,7 +1020,7 @@
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
"integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.27.8"
@@ -869,7 +1033,7 @@
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz",
"integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@jest/schemas": "^29.6.3",
@@ -887,7 +1051,7 @@
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
@@ -909,7 +1073,7 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -919,14 +1083,14 @@
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.30",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
"integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -947,7 +1111,7 @@
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/@lingui/babel-plugin-lingui-macro/-/babel-plugin-lingui-macro-5.4.1.tgz",
"integrity": "sha512-9IO+PDvdneY8OCI8zvI1oDXpzryTMtyRv7uq9O0U1mFCvIPVd5dWQKQDu/CpgpYAc2+JG/izn5PNl9xzPc6ckw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.20.12",
@@ -1167,7 +1331,7 @@
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/@lingui/conf/-/conf-5.4.1.tgz",
"integrity": "sha512-aDkj/bMSr/mCL8Nr1TS52v0GLCuVa4YqtRz+WvUCFZw/ovVInX0hKq1TClx/bSlhu60FzB/CbclxFMBw8aLVUg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.20.13",
@@ -2586,7 +2750,7 @@
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/@swc/core": {
@@ -3256,14 +3420,14 @@
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
"integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/istanbul-lib-report": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz",
"integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/istanbul-lib-coverage": "*"
@@ -3273,7 +3437,7 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz",
"integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/istanbul-lib-report": "*"
@@ -3283,7 +3447,7 @@
"version": "24.3.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
"integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.10.0"
@@ -3293,7 +3457,7 @@
"version": "19.1.11",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.11.tgz",
"integrity": "sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.0.2"
@@ -3303,7 +3467,7 @@
"version": "19.1.7",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz",
"integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.0.0"
@@ -3313,7 +3477,7 @@
"version": "17.0.33",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
"integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/yargs-parser": "*"
@@ -3323,7 +3487,7 @@
"version": "21.0.3",
"resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz",
"integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/@vitejs/plugin-react-swc": {
@@ -3360,7 +3524,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
@@ -3403,7 +3567,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"devOptional": true,
"license": "Python-2.0"
},
"node_modules/aria-hidden": {
@@ -3498,7 +3662,7 @@
"version": "4.25.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz",
"integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==",
"dev": true,
"devOptional": true,
"funding": [
{
"type": "opencollective",
@@ -3569,7 +3733,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -3579,7 +3743,7 @@
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -3592,7 +3756,7 @@
"version": "1.0.30001727",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
"integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",
"dev": true,
"devOptional": true,
"funding": [
{
"type": "opencollective",
@@ -3613,7 +3777,7 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
@@ -3725,7 +3889,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
@@ -3738,7 +3902,7 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/colors": {
@@ -3755,14 +3919,14 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/cosmiconfig": {
"version": "8.3.6",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
"integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"import-fresh": "^3.3.0",
@@ -3942,7 +4106,7 @@
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -4012,7 +4176,7 @@
"version": "1.5.182",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.182.tgz",
"integrity": "sha512-Lv65Btwv9W4J9pyODI6EWpdnhfvrve/us5h1WspW8B2Fb0366REPtY3hX7ounk1CkV/TBjWCEvCBBbYbmV0qCA==",
"dev": true,
"devOptional": true,
"license": "ISC"
},
"node_modules/emoji-regex": {
@@ -4040,7 +4204,7 @@
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"is-arrayish": "^0.2.1"
@@ -4109,7 +4273,7 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -4197,7 +4361,7 @@
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -4223,7 +4387,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -4254,7 +4418,7 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"parent-module": "^1.0.0",
@@ -4297,7 +4461,7 @@
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/is-binary-path": {
@@ -4390,7 +4554,7 @@
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz",
"integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
@@ -4400,7 +4564,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz",
"integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@jest/types": "^29.6.3",
@@ -4418,7 +4582,7 @@
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true,
"devOptional": true,
"license": "MIT",
"bin": {
"jiti": "bin/jiti.js"
@@ -4440,7 +4604,7 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -4453,7 +4617,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
@@ -4466,14 +4630,14 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"bin": {
"json5": "lib/cli.js"
@@ -4486,7 +4650,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
"integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -4735,7 +4899,7 @@
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/lodash": {
@@ -4784,7 +4948,7 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"dev": true,
"devOptional": true,
"license": "ISC",
"dependencies": {
"yallist": "^3.0.2"
@@ -4895,7 +5059,7 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/nanoid": {
@@ -4936,7 +5100,7 @@
"version": "2.0.19",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/normalize-path": {
@@ -5032,7 +5196,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"callsites": "^3.0.0"
@@ -5045,7 +5209,7 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.0.0",
@@ -5074,7 +5238,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -5084,7 +5248,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"devOptional": true,
"license": "ISC"
},
"node_modules/picomatch": {
@@ -5146,7 +5310,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
"integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@jest/schemas": "^29.6.3",
@@ -5161,7 +5325,7 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -5407,7 +5571,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -5505,7 +5669,7 @@
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"devOptional": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -5688,7 +5852,7 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
@@ -5763,14 +5927,14 @@
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.4.4",
"picomatch": "^4.0.2"
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">=12.0.0"
@@ -5822,7 +5986,7 @@
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -5836,14 +6000,14 @@
"version": "7.10.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/update-browserslist-db": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
"dev": true,
"devOptional": true,
"funding": [
{
"type": "opencollective",
@@ -5957,9 +6121,9 @@
}
},
"node_modules/vite": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz",
"integrity": "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==",
"version": "7.1.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz",
"integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5968,7 +6132,7 @@
"picomatch": "^4.0.3",
"postcss": "^8.5.6",
"rollup": "^4.43.0",
"tinyglobby": "^0.2.14"
"tinyglobby": "^0.2.15"
},
"bin": {
"vite": "bin/vite.js"
@@ -6175,7 +6339,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true,
"devOptional": true,
"license": "ISC"
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "beszel",
"private": true,
"version": "0.12.7",
"version": "0.12.12",
"type": "module",
"scripts": {
"dev": "vite --host",
@@ -76,4 +76,4 @@
"optionalDependencies": {
"@esbuild/linux-arm64": "^0.21.5"
}
}
}

View File

@@ -1,5 +1,9 @@
import { Trans } from "@lingui/react/macro"
import { t } from "@lingui/core/macro"
import { Trans } from "@lingui/react/macro"
import { useStore } from "@nanostores/react"
import { getPagePath } from "@nanostores/router"
import { ChevronDownIcon, ExternalLinkIcon, PlusIcon } from "lucide-react"
import { memo, useEffect, useRef, useState } from "react"
import { Button } from "@/components/ui/button"
import {
Dialog,
@@ -10,34 +14,30 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { isReadOnlyUser, pb } from "@/lib/api"
import { SystemStatus } from "@/lib/enums"
import { $publicKey } from "@/lib/stores"
import { cn, generateToken, tokenMap, useBrowserStorage } from "@/lib/utils"
import { pb, isReadOnlyUser } from "@/lib/api"
import { useStore } from "@nanostores/react"
import { ChevronDownIcon, ExternalLinkIcon, PlusIcon } from "lucide-react"
import { memo, useEffect, useRef, useState } from "react"
import { $router, basePath, Link, navigate } from "./router"
import { SystemRecord } from "@/types"
import { SystemStatus } from "@/lib/enums"
import { AppleIcon, DockerIcon, FreeBsdIcon, TuxIcon, WindowsIcon } from "./ui/icons"
import { InputCopy } from "./ui/input-copy"
import { getPagePath } from "@nanostores/router"
import type { SystemRecord } from "@/types"
import {
copyDockerCompose,
copyDockerRun,
copyLinuxCommand,
copyWindowsCommand,
DropdownItem,
type DropdownItem,
InstallDropdown,
} from "./install-dropdowns"
import { $router, basePath, Link, navigate } from "./router"
import { DropdownMenu, DropdownMenuTrigger } from "./ui/dropdown-menu"
import { AppleIcon, DockerIcon, FreeBsdIcon, TuxIcon, WindowsIcon } from "./ui/icons"
import { InputCopy } from "./ui/input-copy"
export function AddSystemButton({ className }: { className?: string }) {
const [open, setOpen] = useState(false)
let opened = useRef(false)
const opened = useRef(false)
if (open) {
opened.current = true
}

View File

@@ -1,11 +1,11 @@
import { ColumnDef } from "@tanstack/react-table"
import { AlertsHistoryRecord } from "@/types"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { formatShortDate, toFixedFloat, formatDuration, cn } from "@/lib/utils"
import { alertInfo } from "@/lib/alerts"
import { Trans } from "@lingui/react/macro"
import { t } from "@lingui/core/macro"
import { Trans } from "@lingui/react/macro"
import type { ColumnDef } from "@tanstack/react-table"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { alertInfo } from "@/lib/alerts"
import { cn, formatDuration, formatShortDate, toFixedFloat } from "@/lib/utils"
import type { AlertsHistoryRecord } from "@/types"
export const alertsHistoryColumns: ColumnDef<AlertsHistoryRecord>[] = [
{
@@ -38,7 +38,7 @@ export const alertsHistoryColumns: ColumnDef<AlertsHistoryRecord>[] = [
</Button>
),
cell: ({ getValue, row }) => {
let name = getValue() as string
const name = getValue() as string
const info = alertInfo[row.original.name]
const Icon = info?.icon

View File

@@ -1,13 +1,13 @@
import { t } from "@lingui/core/macro"
import { memo, useMemo, useState } from "react"
import { useStore } from "@nanostores/react"
import { $alerts } from "@/lib/stores"
import { BellIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { memo, useMemo, useState } from "react"
import { Button } from "@/components/ui/button"
import { SystemRecord } from "@/types"
import { AlertDialogContent } from "./alerts-sheet"
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
import { $alerts } from "@/lib/stores"
import { cn } from "@/lib/utils"
import type { SystemRecord } from "@/types"
import { AlertDialogContent } from "./alerts-sheet"
export default memo(function AlertsButton({ system }: { system: SystemRecord }) {
const [opened, setOpened] = useState(false)

View File

@@ -1,21 +1,20 @@
import { t } from "@lingui/core/macro"
import { Trans, Plural } from "@lingui/react/macro"
import { $alerts, $systems } from "@/lib/stores"
import { cn, debounce } from "@/lib/utils"
import { alertInfo } from "@/lib/alerts"
import { Switch } from "@/components/ui/switch"
import { AlertInfo, AlertRecord, SystemRecord } from "@/types"
import { lazy, memo, Suspense, useMemo, useState } from "react"
import { toast } from "@/components/ui/use-toast"
import { Plural, Trans } from "@lingui/react/macro"
import { useStore } from "@nanostores/react"
import { getPagePath } from "@nanostores/router"
import { Checkbox } from "@/components/ui/checkbox"
import { DialogTitle, DialogDescription } from "@/components/ui/dialog"
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
import { ServerIcon, GlobeIcon } from "lucide-react"
import { GlobeIcon, ServerIcon } from "lucide-react"
import { lazy, memo, Suspense, useMemo, useState } from "react"
import { $router, Link } from "@/components/router"
import { DialogHeader } from "@/components/ui/dialog"
import { Checkbox } from "@/components/ui/checkbox"
import { DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Switch } from "@/components/ui/switch"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { toast } from "@/components/ui/use-toast"
import { alertInfo } from "@/lib/alerts"
import { pb } from "@/lib/api"
import { $alerts, $systems } from "@/lib/stores"
import { cn, debounce } from "@/lib/utils"
import type { AlertInfo, AlertRecord, SystemRecord } from "@/types"
const Slider = lazy(() => import("@/components/ui/slider"))
@@ -172,7 +171,7 @@ export function AlertContent({
const [checked, setChecked] = useState(global ? false : !!alert)
const [min, setMin] = useState(alert?.min || 10)
const [value, setValue] = useState(alert?.value || (singleDescription ? 0 : alertData.start ?? 80))
const [value, setValue] = useState(alert?.value || (singleDescription ? 0 : (alertData.start ?? 80)))
const Icon = alertData.icon

View File

@@ -1,9 +1,16 @@
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
import { cn, formatShortDate, chartMargin } from "@/lib/utils"
import { useYAxisWidth } from "./hooks"
import { ChartData, SystemStatsRecord } from "@/types"
import { useMemo } from "react"
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
xAxis,
} from "@/components/ui/chart"
import { chartMargin, cn, formatShortDate } from "@/lib/utils"
import type { ChartData, SystemStatsRecord } from "@/types"
import { useYAxisWidth } from "./hooks"
export type DataPoint = {
label: string
@@ -20,6 +27,8 @@ export default function AreaChartDefault({
contentFormatter,
dataPoints,
domain,
legend,
itemSorter,
}: // logRender = false,
{
chartData: ChartData
@@ -29,10 +38,13 @@ export default function AreaChartDefault({
contentFormatter: ({ value, payload }: { value: number; payload: SystemStatsRecord }) => string
dataPoints?: DataPoint[]
domain?: [number, number]
legend?: boolean
itemSorter?: (a: any, b: any) => number
// logRender?: boolean
}) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
// biome-ignore lint/correctness/useExhaustiveDependencies: ignore
return useMemo(() => {
if (chartData.systemStats.length === 0) {
return null
@@ -63,6 +75,8 @@ export default function AreaChartDefault({
<ChartTooltip
animationEasing="ease-out"
animationDuration={150}
// @ts-expect-error
itemSorter={itemSorter}
content={
<ChartTooltipContent
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
@@ -70,11 +84,14 @@ export default function AreaChartDefault({
/>
}
/>
{dataPoints?.map((dataPoint, i) => {
const color = `var(--chart-${dataPoint.color})`
{dataPoints?.map((dataPoint) => {
let { color } = dataPoint
if (typeof color === "number") {
color = `var(--chart-${color})`
}
return (
<Area
key={i}
key={dataPoint.label}
dataKey={dataPoint.dataKey}
name={dataPoint.label}
type="monotoneX"
@@ -85,7 +102,7 @@ export default function AreaChartDefault({
/>
)
})}
{/* <ChartLegend content={<ChartLegendContent />} /> */}
{legend && <ChartLegend content={<ChartLegendContent />} />}
</AreaChart>
</ChartContainer>
</div>

View File

@@ -1,9 +1,9 @@
import { useStore } from "@nanostores/react"
import { HistoryIcon } from "lucide-react"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { $chartTime } from "@/lib/stores"
import { chartTimeData, cn } from "@/lib/utils"
import { ChartTimes } from "@/types"
import { useStore } from "@nanostores/react"
import { HistoryIcon } from "lucide-react"
import type { ChartTimes } from "@/types"
export default function ChartTimeSelect({ className }: { className?: string }) {
const chartTime = useStore($chartTime)

View File

@@ -1,13 +1,13 @@
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
import { memo, useMemo } from "react"
import { cn, formatShortDate, chartMargin, toFixedFloat, formatBytes, decimalString } from "@/lib/utils"
// import Spinner from '../spinner'
import { useStore } from "@nanostores/react"
import { memo, useMemo } from "react"
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
import { ChartType, Unit } from "@/lib/enums"
import { $containerFilter, $userSettings } from "@/lib/stores"
import { chartMargin, cn, decimalString, formatBytes, formatShortDate, toFixedFloat } from "@/lib/utils"
import type { ChartData } from "@/types"
import { Separator } from "../ui/separator"
import { ChartType, Unit } from "@/lib/enums"
import { useYAxisWidth } from "./hooks"
export default memo(function ContainerChart({

View File

@@ -1,10 +1,10 @@
import { useLingui } from "@lingui/react/macro"
import { memo } from "react"
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
import { cn, formatShortDate, decimalString, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils"
import { ChartData } from "@/types"
import { memo } from "react"
import { useLingui } from "@lingui/react/macro"
import { Unit } from "@/lib/enums"
import { chartMargin, cn, decimalString, formatBytes, formatShortDate, toFixedFloat } from "@/lib/utils"
import type { ChartData } from "@/types"
import { useYAxisWidth } from "./hooks"
export default memo(function DiskChart({

View File

@@ -1,5 +1,5 @@
import { memo, useMemo } from "react"
import { CartesianGrid, Line, LineChart, YAxis } from "recharts"
import {
ChartContainer,
ChartLegend,
@@ -8,48 +8,59 @@ import {
ChartTooltipContent,
xAxis,
} from "@/components/ui/chart"
import { cn, formatShortDate, toFixedFloat, decimalString, chartMargin } from "@/lib/utils"
import { ChartData } from "@/types"
import { memo, useMemo } from "react"
import { chartMargin, cn, decimalString, formatShortDate, toFixedFloat } from "@/lib/utils"
import type { ChartData, GPUData } from "@/types"
import { useYAxisWidth } from "./hooks"
import type { DataPoint } from "./line-chart"
export default memo(function GpuPowerChart({ chartData }: { chartData: ChartData }) {
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) {
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 (let data of chartData.systemStats) {
let newData = { created: data.created } as Record<string, number | string>
for (let 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 (let 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 (
<div>
<ChartContainer
@@ -57,7 +68,7 @@ export default memo(function GpuPowerChart({ chartData }: { chartData: ChartData
"opacity-100": yAxisWidth,
})}
>
<LineChart accessibilityLayer data={newChartData.data} margin={chartMargin}>
<LineChart accessibilityLayer data={gpuData} margin={chartMargin}>
<CartesianGrid vertical={false} />
<YAxis
direction="ltr"
@@ -67,7 +78,7 @@ export default memo(function GpuPowerChart({ chartData }: { chartData: ChartData
width={yAxisWidth}
tickFormatter={(value) => {
const val = toFixedFloat(value, 2)
return updateYAxisWidth(val + "W")
return updateYAxisWidth(`${val}W`)
}}
tickLine={false}
axisLine={false}
@@ -76,29 +87,29 @@ export default memo(function GpuPowerChart({ chartData }: { chartData: ChartData
<ChartTooltip
animationEasing="ease-out"
animationDuration={150}
// @ts-ignore
// @ts-expect-error
itemSorter={(a, b) => b.value - a.value}
content={
<ChartTooltipContent
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
contentFormatter={(item) => decimalString(item.value) + "W"}
contentFormatter={(item) => `${decimalString(item.value)}W`}
// indicator="line"
/>
}
/>
{colors.map((key) => (
{dataPoints.map((dataPoint) => (
<Line
key={key}
dataKey={key}
name={key}
key={dataPoint.label}
dataKey={dataPoint.dataKey}
name={dataPoint.label}
type="monotoneX"
dot={false}
strokeWidth={1.5}
stroke={newChartData.colors[key]}
stroke={dataPoint.color as string}
isAnimationActive={false}
/>
))}
{colors.length > 1 && <ChartLegend content={<ChartLegendContent />} />}
{dataPoints.length > 1 && <ChartLegend content={<ChartLegendContent />} />}
</LineChart>
</ChartContainer>
</div>

View File

@@ -1,6 +1,6 @@
import { useMemo, useState } from "react"
import { ChartConfig } from "@/components/ui/chart"
import { ChartData } from "@/types"
import type { ChartConfig } from "@/components/ui/chart"
import type { ChartData, SystemStats, SystemStatsRecord } from "@/types"
/** Chart configurations for CPU, memory, and network usage charts */
export interface ContainerChartConfigs {
@@ -105,3 +105,21 @@ export function useYAxisWidth() {
}
return { yAxisWidth, updateYAxisWidth }
}
// Assures consistent colors for network interfaces
export function useNetworkInterfaces(interfaces: SystemStats["ni"]) {
const keys = Object.keys(interfaces ?? {})
const sortedKeys = keys.sort((a, b) => (interfaces?.[b]?.[3] ?? 0) - (interfaces?.[a]?.[3] ?? 0))
return {
length: sortedKeys.length,
data: (index = 3) => {
return sortedKeys.map((key) => ({
label: key,
dataKey: ({ stats }: SystemStatsRecord) => stats?.ni?.[key]?.[index],
color: `hsl(${220 + (((sortedKeys.indexOf(key) * 360) / sortedKeys.length) % 360)}, 70%, 50%)`,
opacity: 0.3,
}))
},
}
}

View File

@@ -0,0 +1,110 @@
import { useMemo } from "react"
import { CartesianGrid, Line, LineChart, YAxis } from "recharts"
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
xAxis,
} from "@/components/ui/chart"
import { chartMargin, cn, formatShortDate } from "@/lib/utils"
import type { ChartData, SystemStatsRecord } from "@/types"
import { useYAxisWidth } from "./hooks"
export type DataPoint = {
label: string
dataKey: (data: SystemStatsRecord) => number | undefined
color: number | string
}
export default function LineChartDefault({
chartData,
max,
maxToggled,
tickFormatter,
contentFormatter,
dataPoints,
domain,
legend,
itemSorter,
}: // logRender = false,
{
chartData: ChartData
max?: number
maxToggled?: boolean
tickFormatter: (value: number, index: number) => string
contentFormatter: ({ value, payload }: { value: number; payload: SystemStatsRecord }) => string
dataPoints?: DataPoint[]
domain?: [number, number]
legend?: boolean
itemSorter?: (a: any, b: any) => number
// logRender?: boolean
}) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
// biome-ignore lint/correctness/useExhaustiveDependencies: ignore
return useMemo(() => {
if (chartData.systemStats.length === 0) {
return null
}
// if (logRender) {
// console.log("Rendered at", new Date())
// }
return (
<div>
<ChartContainer
className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
"opacity-100": yAxisWidth,
})}
>
<LineChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}>
<CartesianGrid vertical={false} />
<YAxis
direction="ltr"
orientation={chartData.orientation}
className="tracking-tighter"
width={yAxisWidth}
domain={domain ?? [0, max ?? "auto"]}
tickFormatter={(value, index) => updateYAxisWidth(tickFormatter(value, index))}
tickLine={false}
axisLine={false}
/>
{xAxis(chartData)}
<ChartTooltip
animationEasing="ease-out"
animationDuration={150}
// @ts-expect-error
itemSorter={itemSorter}
content={
<ChartTooltipContent
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
contentFormatter={contentFormatter}
/>
}
/>
{dataPoints?.map((dataPoint) => {
let { color } = dataPoint
if (typeof color === "number") {
color = `var(--chart-${color})`
}
return (
<Line
key={dataPoint.label}
dataKey={dataPoint.dataKey}
name={dataPoint.label}
type="monotoneX"
dot={false}
strokeWidth={1.5}
stroke={color}
isAnimationActive={false}
/>
)
})}
{legend && <ChartLegend content={<ChartLegendContent />} />}
</LineChart>
</ChartContainer>
</div>
)
}, [chartData.systemStats.at(-1), yAxisWidth, maxToggled])
}

View File

@@ -1,5 +1,6 @@
import { t } from "@lingui/core/macro"
import { memo } from "react"
import { CartesianGrid, Line, LineChart, YAxis } from "recharts"
import {
ChartContainer,
ChartLegend,
@@ -8,10 +9,8 @@ import {
ChartTooltipContent,
xAxis,
} from "@/components/ui/chart"
import { cn, formatShortDate, toFixedFloat, decimalString, chartMargin } from "@/lib/utils"
import { ChartData, SystemStats } from "@/types"
import { memo } from "react"
import { t } from "@lingui/core/macro"
import { chartMargin, cn, decimalString, formatShortDate, toFixedFloat } from "@/lib/utils"
import type { ChartData, SystemStats } from "@/types"
import { useYAxisWidth } from "./hooks"
export default memo(function LoadAverageChart({ chartData }: { chartData: ChartData }) {
@@ -60,7 +59,7 @@ export default memo(function LoadAverageChart({ chartData }: { chartData: ChartD
<ChartTooltip
animationEasing="ease-out"
animationDuration={150}
// @ts-ignore
// @ts-expect-error
// itemSorter={(a, b) => b.value - a.value}
content={
<ChartTooltipContent

View File

@@ -1,10 +1,10 @@
import { useLingui } from "@lingui/react/macro"
import { memo } from "react"
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
import { cn, decimalString, formatShortDate, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils"
import { memo } from "react"
import { ChartData } from "@/types"
import { useLingui } from "@lingui/react/macro"
import { Unit } from "@/lib/enums"
import { chartMargin, cn, decimalString, formatBytes, formatShortDate, toFixedFloat } from "@/lib/utils"
import type { ChartData } from "@/types"
import { useYAxisWidth } from "./hooks"
export default memo(function MemChart({ chartData, showMax }: { chartData: ChartData; showMax: boolean }) {
@@ -53,7 +53,7 @@ export default memo(function MemChart({ chartData, showMax }: { chartData: Chart
animationDuration={150}
content={
<ChartTooltipContent
// @ts-ignore
// @ts-expect-error
itemSorter={(a, b) => a.order - b.order}
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
contentFormatter={({ value }) => {

View File

@@ -1,12 +1,11 @@
import { t } from "@lingui/core/macro"
import { useStore } from "@nanostores/react"
import { memo } from "react"
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
import { cn, formatShortDate, decimalString, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils"
import { ChartData } from "@/types"
import { memo } from "react"
import { $userSettings } from "@/lib/stores"
import { useStore } from "@nanostores/react"
import { chartMargin, cn, decimalString, formatBytes, formatShortDate, toFixedFloat } from "@/lib/utils"
import type { ChartData } from "@/types"
import { useYAxisWidth } from "./hooks"
export default memo(function SwapChart({ chartData }: { chartData: ChartData }) {

View File

@@ -0,0 +1,195 @@
import { SystemdService } from "@/types";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Trans } from "@lingui/react/macro";
import { memo, useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import { ChevronDownIcon, ChevronsUpDownIcon, XIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
interface SystemdServicesTableProps {
services: SystemdService[];
}
type SortKey = "name" | "status" | "cpu" | "mem";
const statusPriority: { [key: string]: number } = {
failed: 1,
activating: 2,
active: 3,
deactivating: 4,
inactive: 5,
};
const getStatusColor = (status: string) => {
switch (status) {
case 'active':
return 'text-green-500';
case 'failed':
return 'text-red-500';
case 'activating':
case 'reloading':
return 'text-blue-500';
case 'inactive':
case 'deactivating':
return 'text-gray-500';
default:
return '';
}
};
const getStatusDotColor = (status: string) => {
switch (status) {
case 'active':
return 'bg-green-500';
case 'failed':
return 'bg-red-500';
case 'activating':
case 'reloading':
return 'bg-blue-500';
case 'inactive':
case 'deactivating':
return 'bg-gray-500';
default:
return 'bg-gray-400';
}
};
export default memo(function SystemdServicesTable({ services }: SystemdServicesTableProps) {
const [isExpanded, setIsExpanded] = useState(false);
const [filter, setFilter] = useState("");
const [sortKey, setSortKey] = useState<SortKey>("status");
const [sortAsc, setSortAsc] = useState(true);
const handleSort = (key: SortKey) => {
if (sortKey === key) {
setSortAsc(!sortAsc);
} else {
setSortKey(key);
setSortAsc(true);
}
};
const sortedServices = useMemo(() => {
return [...services].sort((a, b) => {
let compare = 0;
switch (sortKey) {
case "name":
compare = a.n.localeCompare(b.n);
break;
case "status":
const priorityA = statusPriority[a.s] || 99;
const priorityB = statusPriority[b.s] || 99;
compare = priorityA - priorityB;
if (compare === 0) {
compare = a.n.localeCompare(b.n);
}
break;
case "cpu":
compare = (a.c ?? 0) - (b.c ?? 0);
break;
case "mem":
compare = (a.m ?? 0) - (b.m ?? 0);
break;
}
return sortAsc ? compare : -compare;
});
}, [services, sortKey, sortAsc]);
const failedServices = useMemo(() => sortedServices.filter(s => s.s === 'failed'), [sortedServices]);
const activeServicesCount = useMemo(() => services.filter(s => s.s === 'active').length, [services]);
const filteredServices = useMemo(() => {
if (!filter) {
return sortedServices;
}
return sortedServices.filter(service => service.n.toLowerCase().includes(filter.toLowerCase()));
}, [sortedServices, filter]);
const servicesToShow = isExpanded ? filteredServices : failedServices;
const summary = (
<span className="text-sm text-muted-foreground ml-2">
({failedServices.length} <Trans>failed</Trans>, {activeServicesCount} <Trans>active</Trans>)
</span>
);
const SortableHeader = ({ sortKey: key, children }: { sortKey: SortKey, children: React.ReactNode }) => (
<TableHead onClick={() => handleSort(key)} className="cursor-pointer">
<div className="flex items-center gap-1">
{children}
<ChevronsUpDownIcon className="h-3 w-3" />
</div>
</TableHead>
);
return (
<div>
<div className="flex justify-between items-center mb-2">
<h3 className="text-lg font-semibold">
<Trans>Systemd Services</Trans> {summary}
</h3>
{isExpanded && (
<div className="relative max-w-xs">
<Input
placeholder="Filter services..."
value={filter}
onChange={(e) => setFilter(e.target.value)}
className="ps-4 pe-8"
/>
{filter && (
<Button
type="button"
variant="ghost"
size="icon"
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"
onClick={() => setFilter("")}
>
<XIcon className="h-4 w-4" />
</Button>
)}
</div>
)}
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<SortableHeader sortKey="name"><Trans>Service</Trans></SortableHeader>
<SortableHeader sortKey="status"><Trans>Status</Trans></SortableHeader>
<SortableHeader sortKey="cpu"><Trans>CPU Usage</Trans></SortableHeader>
<SortableHeader sortKey="mem"><Trans>Memory</Trans></SortableHeader>
</TableRow>
</TableHeader>
<TableBody>
{servicesToShow.map((service) => (
<TableRow key={service.n}>
<TableCell className="font-medium">{service.n}</TableCell>
<TableCell className={cn("flex items-center gap-2", getStatusColor(service.s))}>
<span className={cn("h-2 w-2 rounded-full", getStatusDotColor(service.s))} />
{service.s}
</TableCell>
<TableCell>{(service.c ?? 0).toFixed(2)}%</TableCell>
<TableCell>{(service.m ?? 0).toFixed(2)} MB</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{failedServices.length === 0 && !isExpanded && (
<div className="text-center py-4 text-muted-foreground">
<Trans>No failed services.</Trans>
</div>
)}
<div className="flex justify-center mt-2">
<Button variant="ghost" onClick={() => setIsExpanded(!isExpanded)} className="flex items-center gap-1">
<span>
{isExpanded ? <Trans>Show less</Trans> : <Trans>Show all</Trans>} ({services.length})
</span>
<ChevronDownIcon className={cn("h-4 w-4 transition-transform", isExpanded && "rotate-180")} />
</Button>
</div>
</div>
);
})

View File

@@ -1,5 +1,6 @@
import { useStore } from "@nanostores/react"
import { memo, useMemo } from "react"
import { CartesianGrid, Line, LineChart, YAxis } from "recharts"
import {
ChartContainer,
ChartLegend,
@@ -8,11 +9,9 @@ import {
ChartTooltipContent,
xAxis,
} from "@/components/ui/chart"
import { cn, formatShortDate, toFixedFloat, chartMargin, formatTemperature, decimalString } from "@/lib/utils"
import { ChartData } from "@/types"
import { memo, useMemo } from "react"
import { $temperatureFilter, $userSettings } from "@/lib/stores"
import { useStore } from "@nanostores/react"
import { chartMargin, cn, decimalString, formatShortDate, formatTemperature, toFixedFloat } from "@/lib/utils"
import type { ChartData } from "@/types"
import { useYAxisWidth } from "./hooks"
export default memo(function TemperatureChart({ chartData }: { chartData: ChartData }) {
@@ -31,18 +30,18 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD
colors: Record<string, string>
}
const tempSums = {} as Record<string, number>
for (let data of chartData.systemStats) {
let newData = { created: data.created } as Record<string, number | string>
let keys = Object.keys(data.stats?.t ?? {})
for (const data of chartData.systemStats) {
const newData = { created: data.created } as Record<string, number | string>
const keys = Object.keys(data.stats?.t ?? {})
for (let i = 0; i < keys.length; i++) {
let key = keys[i]
const key = keys[i]
newData[key] = data.stats.t![key]
tempSums[key] = (tempSums[key] ?? 0) + newData[key]
}
newChartData.data.push(newData)
}
const keys = Object.keys(tempSums).sort((a, b) => tempSums[b] - tempSums[a])
for (let key of keys) {
for (const key of keys) {
newChartData.colors[key] = `hsl(${((keys.indexOf(key) * 360) / keys.length) % 360}, 60%, 55%)`
}
return newChartData
@@ -78,7 +77,7 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD
<ChartTooltip
animationEasing="ease-out"
animationDuration={150}
// @ts-ignore
// @ts-expect-error
itemSorter={(a, b) => b.value - a.value}
content={
<ChartTooltipContent
@@ -93,7 +92,7 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD
/>
{colors.map((key) => {
const filtered = filter && !key.toLowerCase().includes(filter.toLowerCase())
let strokeOpacity = filtered ? 0.1 : 1
const strokeOpacity = filtered ? 0.1 : 1
return (
<Line
key={key}

View File

@@ -1,3 +1,7 @@
import { t } from "@lingui/core/macro"
import { Trans } from "@lingui/react/macro"
import { getPagePath } from "@nanostores/router"
import { DialogDescription } from "@radix-ui/react-dialog"
import {
AlertOctagonIcon,
BookIcon,
@@ -10,7 +14,7 @@ import {
SettingsIcon,
UsersIcon,
} from "lucide-react"
import { memo, useEffect, useMemo } from "react"
import {
CommandDialog,
CommandEmpty,
@@ -21,15 +25,10 @@ import {
CommandSeparator,
CommandShortcut,
} from "@/components/ui/command"
import { memo, useEffect, useMemo } from "react"
import { isAdmin } from "@/lib/api"
import { $systems } from "@/lib/stores"
import { getHostDisplayValue, listen } from "@/lib/utils"
import { $router, basePath, navigate, prependBasePath } from "./router"
import { Trans } from "@lingui/react/macro"
import { t } from "@lingui/core/macro"
import { getPagePath } from "@nanostores/router"
import { DialogDescription } from "@radix-ui/react-dialog"
import { isAdmin } from "@/lib/api"
export default memo(function CommandPalette({ open, setOpen }: { open: boolean; setOpen: (open: boolean) => void }) {
useEffect(() => {

View File

@@ -1,8 +1,8 @@
import { Trans } from "@lingui/react/macro";
import { Trans } from "@lingui/react/macro"
import { useEffect, useMemo, useRef } from "react"
import { $copyContent } from "@/lib/stores"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog"
import { Textarea } from "./ui/textarea"
import { $copyContent } from "@/lib/stores"
export default function CopyToClipboard({ content }: { content: string }) {
return (

View File

@@ -1,7 +1,7 @@
import { memo } from "react"
import { DropdownMenuContent, DropdownMenuItem } from "./ui/dropdown-menu"
import { copyToClipboard, getHubURL } from "@/lib/utils"
import { i18n } from "@lingui/core"
import { memo } from "react"
import { copyToClipboard, getHubURL } from "@/lib/utils"
import { DropdownMenuContent, DropdownMenuItem } from "./ui/dropdown-menu"
// const isbeta = beszel.hub_version.includes("beta")
// const imagetag = isbeta ? ":edge" : ""

View File

@@ -1,11 +1,10 @@
import { useLingui } from "@lingui/react/macro"
import { LanguagesIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { dynamicActivate } from "@/lib/i18n"
import languages from "@/lib/languages"
import { cn } from "@/lib/utils"
import { useLingui } from "@lingui/react/macro"
import { dynamicActivate } from "@/lib/i18n"
export function LangToggle() {
const { i18n } = useLingui()

View File

@@ -1,19 +1,19 @@
import { t } from "@lingui/core/macro"
import { Trans } from "@lingui/react/macro"
import { cn } from "@/lib/utils"
import { getPagePath } from "@nanostores/router"
import { KeyIcon, LoaderCircle, LockIcon, LogInIcon, MailIcon } from "lucide-react"
import type { AuthMethodsList, AuthProviderInfo, OAuth2AuthConfig } from "pocketbase"
import { useCallback, useEffect, useState } from "react"
import * as v from "valibot"
import { buttonVariants } from "@/components/ui/button"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { KeyIcon, LoaderCircle, LockIcon, LogInIcon, MailIcon } from "lucide-react"
import { $authenticated } from "@/lib/stores"
import * as v from "valibot"
import { toast } from "../ui/use-toast"
import { Dialog, DialogContent, DialogTrigger, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { useCallback, useEffect, useState } from "react"
import { AuthMethodsList, AuthProviderInfo, OAuth2AuthConfig } from "pocketbase"
import { $router, Link, prependBasePath } from "../router"
import { getPagePath } from "@nanostores/router"
import { pb } from "@/lib/api"
import { $authenticated } from "@/lib/stores"
import { cn } from "@/lib/utils"
import { $router, Link, prependBasePath } from "../router"
import { toast } from "../ui/use-toast"
import { OtpInputForm } from "./otp-forms"
const honeypot = v.literal("")
@@ -83,9 +83,9 @@ export function UserAuthForm({
const result = v.safeParse(Schema, data)
if (!result.success) {
console.log(result)
let errors = {}
const errors = {}
for (const issue of result.issues) {
// @ts-ignore
// @ts-expect-error
errors[issue.path[0].key] = issue.message
}
setErrors(errors)
@@ -96,7 +96,7 @@ export function UserAuthForm({
if (isFirstRun) {
// check that passwords match
if (password !== passwordConfirm) {
let msg = "Passwords do not match"
const msg = "Passwords do not match"
setErrors({ passwordConfirm: msg })
return
}

View File

@@ -1,15 +1,14 @@
import { Trans } from "@lingui/react/macro"
import { t } from "@lingui/core/macro"
import { Trans } from "@lingui/react/macro"
import { LoaderCircle, MailIcon, SendHorizonalIcon } from "lucide-react"
import { useCallback, useState } from "react"
import { pb } from "@/lib/api"
import { cn } from "@/lib/utils"
import { buttonVariants } from "../ui/button"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog"
import { Input } from "../ui/input"
import { Label } from "../ui/label"
import { useCallback, useState } from "react"
import { toast } from "../ui/use-toast"
import { buttonVariants } from "../ui/button"
import { cn } from "@/lib/utils"
import { Dialog, DialogHeader } from "../ui/dialog"
import { DialogContent, DialogTrigger, DialogTitle } from "../ui/dialog"
import { pb } from "@/lib/api"
const showLoginFaliedToast = () => {
toast({

View File

@@ -1,14 +1,14 @@
import { t } from "@lingui/core/macro"
import { UserAuthForm } from "@/components/login/auth-form"
import { Logo } from "../logo"
import { useEffect, useMemo, useState } from "react"
import { useStore } from "@nanostores/react"
import ForgotPassword from "./forgot-pass-form"
import { $router } from "../router"
import { AuthMethodsList } from "pocketbase"
import { useTheme } from "../theme-provider"
import type { AuthMethodsList } from "pocketbase"
import { useEffect, useMemo, useState } from "react"
import { UserAuthForm } from "@/components/login/auth-form"
import { pb } from "@/lib/api"
import { Logo } from "../logo"
import { ModeToggle } from "../mode-toggle"
import { $router } from "../router"
import { useTheme } from "../theme-provider"
import ForgotPassword from "./forgot-pass-form"
import { OtpRequestForm } from "./otp-forms"
export default function () {
@@ -53,7 +53,7 @@ export default function () {
<div className="min-h-svh grid items-center py-12">
<div
className="grid gap-5 w-full px-4 mx-auto"
// @ts-ignore
// @ts-expect-error
style={{ maxWidth: "21.5em", "--border": theme == "light" ? "hsl(30, 8%, 70%)" : "hsl(220, 3%, 25%)" }}
>
<div className="absolute top-3 right-3">

View File

@@ -1,15 +1,15 @@
import { Trans } from "@lingui/react/macro"
import { LoaderCircle, MailIcon, SendHorizonalIcon } from "lucide-react"
import { useCallback, useState } from "react"
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/otp"
import { pb } from "@/lib/api"
import { $authenticated } from "@/lib/stores"
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/otp"
import { Trans } from "@lingui/react/macro"
import { showLoginFaliedToast } from "./auth-form"
import { cn } from "@/lib/utils"
import { MailIcon, LoaderCircle, SendHorizonalIcon } from "lucide-react"
import { Label } from "../ui/label"
import { $router } from "../router"
import { buttonVariants } from "../ui/button"
import { Input } from "../ui/input"
import { $router } from "../router"
import { Label } from "../ui/label"
import { showLoginFaliedToast } from "./auth-form"
export function OtpInputForm({ otpId, mfaId }: { otpId: string; mfaId: string }) {
const [value, setValue] = useState("")

View File

@@ -1,8 +1,7 @@
import { t } from "@lingui/core/macro"
import { MoonStarIcon, SunIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import { useTheme } from "@/components/theme-provider"
import { Button } from "@/components/ui/button"
export function ModeToggle() {
const { theme, setTheme } = useTheme()

View File

@@ -1,6 +1,5 @@
import { Trans } from "@lingui/react/macro"
import { useState, lazy, Suspense } from "react"
import { Button, buttonVariants } from "@/components/ui/button"
import { getPagePath } from "@nanostores/router"
import {
DatabaseBackupIcon,
LogOutIcon,
@@ -11,23 +10,24 @@ import {
UserIcon,
UsersIcon,
} from "lucide-react"
import { $router, basePath, Link, prependBasePath } from "./router"
import { LangToggle } from "./lang-toggle"
import { ModeToggle } from "./mode-toggle"
import { Logo } from "./logo"
import { cn, runOnce } from "@/lib/utils"
import { isReadOnlyUser, isAdmin, logOut, pb } from "@/lib/api"
import { lazy, Suspense, useState } from "react"
import { Button, buttonVariants } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { isAdmin, isReadOnlyUser, logOut, pb } from "@/lib/api"
import { cn, runOnce } from "@/lib/utils"
import { AddSystemButton } from "./add-system"
import { getPagePath } from "@nanostores/router"
import { LangToggle } from "./lang-toggle"
import { Logo } from "./logo"
import { ModeToggle } from "./mode-toggle"
import { $router, basePath, Link, prependBasePath } from "./router"
const CommandPalette = lazy(() => import("./command-palette"))

View File

@@ -23,7 +23,7 @@ export const prependBasePath = (path: string) => (basePath + path).replaceAll("/
// prepend base path to routes
for (const route in routes) {
// @ts-ignore need as const above to get nanostores to parse types properly
// @ts-expect-error need as const above to get nanostores to parse types properly
routes[route] = prependBasePath(routes[route])
}

View File

@@ -3,13 +3,13 @@ import { Trans, useLingui } from "@lingui/react/macro"
import { redirectPage } from "@nanostores/router"
import {
CopyIcon,
ExternalLinkIcon,
FingerprintIcon,
KeyIcon,
MoreHorizontalIcon,
RotateCwIcon,
ServerIcon,
Trash2Icon,
ExternalLinkIcon,
} from "lucide-react"
import { memo, useEffect, useMemo, useState } from "react"
import {

View File

@@ -3,7 +3,15 @@ import { Plural, Trans, useLingui } from "@lingui/react/macro"
import { useStore } from "@nanostores/react"
import { getPagePath } from "@nanostores/router"
import { timeTicks } from "d3-time"
import { ClockArrowUp, CpuIcon, GlobeIcon, LayoutGridIcon, MonitorIcon, XIcon } from "lucide-react"
import {
ChevronRightSquareIcon,
ClockArrowUp,
CpuIcon,
GlobeIcon,
LayoutGridIcon,
MonitorIcon,
XIcon,
} from "lucide-react"
import { subscribeKeys } from "nanostores"
import React, { type JSX, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
import AreaChartDefault from "@/components/charts/area-chart"
@@ -16,7 +24,7 @@ import MemChart from "@/components/charts/mem-chart"
import SwapChart from "@/components/charts/swap-chart"
import TemperatureChart from "@/components/charts/temperature-chart"
import { getPbTimestamp, pb } from "@/lib/api"
import { ChartType, Os, SystemStatus, Unit } from "@/lib/enums"
import { ChartType, ConnectionType, connectionTypeLabels, Os, SystemStatus, Unit } from "@/lib/enums"
import { batteryStateTranslations } from "@/lib/i18n"
import {
$allSystemsByName,
@@ -41,17 +49,20 @@ import {
toFixedFloat,
useBrowserStorage,
} from "@/lib/utils"
import type { ChartData, ChartTimes, ContainerStatsRecord, GPUData, SystemRecord, SystemStatsRecord } from "@/types"
import type { ChartData, ChartTimes, ContainerStatsRecord, GPUData, SystemRecord, SystemStatsRecord, SystemdStatsRecord } from "@/types"
import ChartTimeSelect from "../charts/chart-time-select"
import { $router, navigate } from "../router"
import Spinner from "../spinner"
import { Button } from "../ui/button"
import { Card, CardDescription, CardHeader, CardTitle } from "../ui/card"
import { AppleIcon, ChartAverage, ChartMax, FreeBsdIcon, Rows, TuxIcon, WindowsIcon } from "../ui/icons"
import { AppleIcon, ChartAverage, ChartMax, FreeBsdIcon, Rows, TuxIcon, WebSocketIcon, WindowsIcon } from "../ui/icons"
import { Input } from "../ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
import { Separator } from "../ui/separator"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip"
import NetworkSheet from "./system/network-sheet"
import LineChartDefault from "../charts/line-chart"
import SystemdServicesTable from "../charts/systemd-services-table"
type ChartTimeData = {
time: number
@@ -85,7 +96,7 @@ function getTimeData(chartTime: ChartTimes, lastCreated: number) {
}
// add empty values between records to make gaps if interval is too large
function addEmptyValues<T extends SystemStatsRecord | ContainerStatsRecord>(
function addValues<T extends SystemStatsRecord | ContainerStatsRecord | SystemdStatsRecord>(
prevRecords: T[],
newRecords: T[],
expectedInterval: number
@@ -109,7 +120,7 @@ function addEmptyValues<T extends SystemStatsRecord | ContainerStatsRecord>(
return modifiedRecords
}
async function getStats<T extends SystemStatsRecord | ContainerStatsRecord>(
async function getStats<T extends SystemStatsRecord | ContainerStatsRecord | SystemdStatsRecord>(
collection: string,
system: SystemRecord,
chartTime: ChartTimes
@@ -129,7 +140,7 @@ async function getStats<T extends SystemStatsRecord | ContainerStatsRecord>(
function dockerOrPodman(str: string, system: SystemRecord) {
if (system.info.p) {
str = str.replace("docker", "podman").replace("Docker", "Podman")
return str.replace("docker", "podman").replace("Docker", "Podman")
}
return str
}
@@ -143,6 +154,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
const [grid, setGrid] = useBrowserStorage("grid", true)
const [system, setSystem] = useState({} as SystemRecord)
const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[])
const [systemdStats, setSystemdStats] = useState([] as SystemdStatsRecord[])
const [containerData, setContainerData] = useState([] as ChartData["containerData"])
const netCardRef = useRef<HTMLDivElement>(null)
const persistChartTime = useRef(false)
@@ -161,6 +173,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
}
persistChartTime.current = false
setSystemStats([])
setSystemdStats([])
setContainerData([])
setContainerFilterBar(null)
$containerFilter.set("")
@@ -225,7 +238,8 @@ export default memo(function SystemDetail({ name }: { name: string }) {
Promise.allSettled([
getStats<SystemStatsRecord>("system_stats", system, chartTime),
getStats<ContainerStatsRecord>("container_stats", system, chartTime),
]).then(([systemStats, containerStats]) => {
getStats<SystemdStatsRecord>("systemd_stats", system, chartTime),
]).then(([systemStats, containerStats, systemdStats]) => {
// loading: false
setChartLoading(false)
@@ -234,18 +248,29 @@ export default memo(function SystemDetail({ name }: { name: string }) {
const ss_cache_key = `${system.id}_${chartTime}_system_stats`
let systemData = (cache.get(ss_cache_key) || []) as SystemStatsRecord[]
if (systemStats.status === "fulfilled" && systemStats.value.length) {
systemData = systemData.concat(addEmptyValues(systemData, systemStats.value, expectedInterval))
systemData = systemData.concat(addValues(systemData, systemStats.value, expectedInterval))
if (systemData.length > 120) {
systemData = systemData.slice(-100)
}
cache.set(ss_cache_key, systemData)
}
setSystemStats(systemData)
// make new systemd stats
const sds_cache_key = `${system.id}_${chartTime}_systemd_stats`
let systemdData = (cache.get(sds_cache_key) || []) as SystemdStatsRecord[]
if (systemdStats.status === "fulfilled" && systemdStats.value.length) {
systemdData = systemdData.concat(addValues(systemdData, systemdStats.value, expectedInterval))
if (systemdData.length > 120) {
systemdData = systemdData.slice(-100)
}
cache.set(sds_cache_key, systemdData)
}
setSystemdStats(systemdData)
// make new container stats
const cs_cache_key = `${system.id}_${chartTime}_container_stats`
let containerData = (cache.get(cs_cache_key) || []) as ContainerStatsRecord[]
if (containerStats.status === "fulfilled" && containerStats.value.length) {
containerData = containerData.concat(addEmptyValues(containerData, containerStats.value, expectedInterval))
containerData = containerData.concat(addValues(containerData, containerStats.value, expectedInterval))
if (containerData.length > 120) {
containerData = containerData.slice(-100)
}
@@ -388,7 +413,8 @@ export default memo(function SystemDetail({ name }: { name: string }) {
const dataEmpty = !chartLoading && chartData.systemStats.length === 0
const lastGpuVals = Object.values(systemStats.at(-1)?.stats.g ?? {})
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)
let translatedStatus: string = system.status
if (system.status === SystemStatus.Up) {
@@ -406,25 +432,44 @@ export default memo(function SystemDetail({ name }: { name: string }) {
<div>
<h1 className="text-[1.6rem] font-semibold mb-1.5">{system.name}</h1>
<div className="flex flex-wrap items-center gap-3 gap-y-2 text-sm opacity-90">
<div className="capitalize flex gap-2 items-center">
<span className={cn("relative flex h-3 w-3")}>
{system.status === SystemStatus.Up && (
<span
className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
style={{ animationDuration: "1.5s" }}
></span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="capitalize flex gap-2 items-center">
<span className={cn("relative flex h-3 w-3")}>
{system.status === SystemStatus.Up && (
<span
className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
style={{ animationDuration: "1.5s" }}
></span>
)}
<span
className={cn("relative inline-flex rounded-full h-3 w-3", {
"bg-green-500": system.status === SystemStatus.Up,
"bg-red-500": system.status === SystemStatus.Down,
"bg-primary/40": system.status === SystemStatus.Paused,
"bg-yellow-500": system.status === SystemStatus.Pending,
})}
></span>
</span>
{translatedStatus}
</div>
</TooltipTrigger>
{system.info.ct && (
<TooltipContent>
<div className="flex gap-1 items-center">
{system.info.ct === ConnectionType.WebSocket ? (
<WebSocketIcon className="size-4" />
) : (
<ChevronRightSquareIcon className="size-4" strokeWidth={2} />
)}
{connectionTypeLabels[system.info.ct as ConnectionType]}
</div>
</TooltipContent>
)}
<span
className={cn("relative inline-flex rounded-full h-3 w-3", {
"bg-green-500": system.status === SystemStatus.Up,
"bg-red-500": system.status === SystemStatus.Down,
"bg-primary/40": system.status === SystemStatus.Paused,
"bg-yellow-500": system.status === SystemStatus.Pending,
})}
></span>
</span>
{translatedStatus}
</div>
</Tooltip>
</TooltipProvider>
{systemInfo.map(({ value, label, Icon, hide }) => {
if (hide || !value) {
return null
@@ -564,13 +609,13 @@ export default memo(function SystemDetail({ name }: { name: string }) {
dataPoints={[
{
label: t({ message: "Write", comment: "Disk write" }),
dataKey: ({ stats }) => (showMax ? stats?.dwm : stats?.dw),
dataKey: ({ stats }: SystemStatsRecord) => (showMax ? stats?.dwm : stats?.dw),
color: 3,
opacity: 0.3,
},
{
label: t({ message: "Read", comment: "Disk read" }),
dataKey: ({ stats }) => (showMax ? stats?.drm : stats?.dr),
dataKey: ({ stats }: SystemStatsRecord) => (showMax ? stats?.drm : stats?.dr),
color: 1,
opacity: 0.3,
},
@@ -590,7 +635,12 @@ export default memo(function SystemDetail({ name }: { name: string }) {
empty={dataEmpty}
grid={grid}
title={t`Bandwidth`}
cornerEl={maxValSelect}
cornerEl={
<div className="flex gap-2">
{maxValSelect}
<NetworkSheet chartData={chartData} dataEmpty={dataEmpty} grid={grid} maxValues={maxValues} />
</div>
}
description={t`Network traffic of public interfaces`}
>
<AreaChartDefault
@@ -600,7 +650,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
{
label: t`Sent`,
// use bytes if available, otherwise multiply old MB (can remove in future)
dataKey(data) {
dataKey(data: SystemStatsRecord) {
if (showMax) {
return data?.stats?.bm?.[0] ?? (data?.stats?.nsm ?? 0) * 1024 * 1024
}
@@ -611,7 +661,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
},
{
label: t`Received`,
dataKey(data) {
dataKey(data: SystemStatsRecord) {
if (showMax) {
return data?.stats?.bm?.[1] ?? (data?.stats?.nrm ?? 0) * 1024 * 1024
}
@@ -620,7 +670,9 @@ export default memo(function SystemDetail({ name }: { name: string }) {
color: 2,
opacity: 0.2,
},
]}
]
// try to place the lesser number in front for better visibility
.sort(() => (systemStats.at(-1)?.stats.b?.[1] ?? 0) - (systemStats.at(-1)?.stats.b?.[0] ?? 0))}
tickFormatter={(val) => {
const { value, unit } = formatBytes(val, true, userSettings.unitNet, false)
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
@@ -674,6 +726,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
grid={grid}
title={t`Load Average`}
description={t`System load averages over time`}
legend={true}
>
<LoadAverageChart chartData={chartData} />
</ChartCard>
@@ -687,6 +740,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
title={t`Temperature`}
description={t`Temperatures of system sensors`}
cornerEl={<FilterBar store={$temperatureFilter} />}
legend={Object.keys(systemStats.at(-1)?.stats.t ?? {}).length < 12}
>
<TemperatureChart chartData={chartData} />
</ChartCard>
@@ -720,7 +774,6 @@ export default memo(function SystemDetail({ name }: { name: string }) {
/>
</ChartCard>
)}
{/* GPU power draw chart */}
{hasGpuPowerData && (
<ChartCard
@@ -734,14 +787,26 @@ export default memo(function SystemDetail({ name }: { name: string }) {
)}
</div>
{/* GPU charts */}
{/* Non-power GPU charts */}
{hasGpuData && (
<div className="grid xl:grid-cols-2 gap-4">
{hasGpuEnginesData && (
<ChartCard
legend={true}
empty={dataEmpty}
grid={grid}
title={t`GPU Engines`}
description={t`Average utilization of GPU engines`}
>
<GpuEnginesChart chartData={chartData} />
</ChartCard>
)}
{Object.keys(systemStats.at(-1)?.stats.g ?? {}).map((id) => {
const gpu = systemStats.at(-1)?.stats.g?.[id] as GPUData
return (
<div key={id} className="contents">
<ChartCard
className="!col-span-1"
empty={dataEmpty}
grid={grid}
title={`${gpu.n} ${t`Usage`}`}
@@ -761,39 +826,54 @@ export default memo(function SystemDetail({ name }: { name: string }) {
contentFormatter={({ value }) => `${decimalString(value)}%`}
/>
</ChartCard>
<ChartCard
empty={dataEmpty}
grid={grid}
title={`${gpu.n} VRAM`}
description={t`Precise utilization at the recorded time`}
>
<AreaChartDefault
chartData={chartData}
dataPoints={[
{
label: t`Usage`,
dataKey: ({ stats }) => stats?.g?.[id]?.mu ?? 0,
color: 2,
opacity: 0.25,
},
]}
max={gpu.mt}
tickFormatter={(val) => {
const { value, unit } = formatBytes(val, false, Unit.Bytes, true)
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
}}
contentFormatter={({ value }) => {
const { value: convertedValue, unit } = formatBytes(value, false, Unit.Bytes, true)
return `${decimalString(convertedValue)} ${unit}`
}}
/>
</ChartCard>
{(gpu.mt ?? 0) > 0 && (
<ChartCard
empty={dataEmpty}
grid={grid}
title={`${gpu.n} VRAM`}
description={t`Precise utilization at the recorded time`}
>
<AreaChartDefault
chartData={chartData}
dataPoints={[
{
label: t`Usage`,
dataKey: ({ stats }) => stats?.g?.[id]?.mu ?? 0,
color: 2,
opacity: 0.25,
},
]}
max={gpu.mt}
tickFormatter={(val) => {
const { value, unit } = formatBytes(val, false, Unit.Bytes, true)
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
}}
contentFormatter={({ value }) => {
const { value: convertedValue, unit } = formatBytes(value, false, Unit.Bytes, true)
return `${decimalString(convertedValue)} ${unit}`
}}
/>
</ChartCard>
)}
</div>
)
})}
</div>
)}
{/* systemd services table */}
{(systemdStats.at(-1)?.stats?.length ?? 0) > 0 && (
<Card className="col-span-full">
<CardHeader className="pb-5 pt-4 gap-1 relative max-sm:py-3 max-sm:px-4">
<CardTitle className="text-xl sm:text-2xl"><Trans>Systemd Services</Trans></CardTitle>
</CardHeader>
<div className="px-4 pb-4">
<SystemdServicesTable services={systemdStats.at(-1)!.stats!} />
</div>
</Card>
)}
{/* extra filesystem charts */}
{Object.keys(systemStats.at(-1)?.stats.efs ?? {}).length > 0 && (
<div className="grid xl:grid-cols-2 gap-4">
@@ -859,27 +939,47 @@ export default memo(function SystemDetail({ name }: { name: string }) {
)
})
function GpuEnginesChart({ chartData }: { chartData: ChartData }) {
const dataPoints = []
const engines = Object.keys(chartData.systemStats?.at(-1)?.stats.g?.[0]?.e ?? {}).sort()
for (const engine of engines) {
dataPoints.push({
label: engine,
dataKey: ({ stats }: SystemStatsRecord) => stats?.g?.[0]?.e?.[engine] ?? 0,
color: `hsl(${140 + (((engines.indexOf(engine) * 360) / engines.length) % 360)}, 65%, 52%)`,
opacity: 0.35,
})
}
return (
<LineChartDefault
legend={true}
chartData={chartData}
dataPoints={dataPoints}
tickFormatter={(val) => `${toFixedFloat(val, 2)}%`}
contentFormatter={({ value }) => `${decimalString(value)}%`}
/>
)
}
function FilterBar({ store = $containerFilter }: { store?: typeof $containerFilter }) {
const containerFilter = useStore(store)
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(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
if (inputRef.current) {
inputRef.current.value = value
}
debouncedStoreSet(value)
},
(e: React.ChangeEvent<HTMLInputElement>) => debouncedStoreSet(e.target.value),
[debouncedStoreSet]
)
return (
<>
<Input placeholder={t`Filter...`} className="ps-4 pe-8" onChange={handleChange} ref={inputRef} />
<Input
placeholder={t`Filter...`}
className="ps-4 pe-8 w-full sm:w-44"
onChange={handleChange}
value={containerFilter}
/>
{containerFilter && (
<Button
type="button"
@@ -887,12 +987,7 @@ function FilterBar({ store = $containerFilter }: { store?: typeof $containerFilt
size="icon"
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"
onClick={() => {
if (inputRef.current) {
inputRef.current.value = ""
}
store.set("")
}}
onClick={() => store.set("")}
>
<XIcon className="h-4 w-4" />
</Button>
@@ -905,7 +1000,7 @@ const SelectAvgMax = memo(({ max }: { max: boolean }) => {
const Icon = max ? ChartMax : ChartAverage
return (
<Select value={max ? "max" : "avg"} onValueChange={(e) => $maxValues.set(e === "max")}>
<SelectTrigger className="relative ps-10 pe-5">
<SelectTrigger className="relative ps-10 pe-5 w-full sm:w-44">
<Icon className="h-4 w-4 absolute start-4 top-1/2 -translate-y-1/2 opacity-85" />
<SelectValue />
</SelectTrigger>
@@ -921,13 +1016,15 @@ const SelectAvgMax = memo(({ max }: { max: boolean }) => {
)
})
function ChartCard({
export function ChartCard({
title,
description,
children,
grid,
empty,
cornerEl,
legend,
className,
}: {
title: string
description: string
@@ -935,17 +1032,22 @@ function ChartCard({
grid?: boolean
empty?: boolean
cornerEl?: JSX.Element | null
legend?: boolean
className?: string
}) {
const { isIntersecting, ref } = useIntersectionObserver()
return (
<Card className={cn("pb-2 sm:pb-4 odd:last-of-type:col-span-full", { "col-span-full": !grid })} ref={ref}>
<Card
className={cn("pb-2 sm:pb-4 odd:last-of-type:col-span-full min-h-full", { "col-span-full": !grid }, className)}
ref={ref}
>
<CardHeader className="pb-5 pt-4 gap-1 relative max-sm:py-3 max-sm:px-4">
<CardTitle className="text-xl sm:text-2xl">{title}</CardTitle>
<CardDescription>{description}</CardDescription>
{cornerEl && <div className="relative py-1 block sm:w-44 sm:absolute sm:top-3.5 sm:end-3.5">{cornerEl}</div>}
{cornerEl && <div className="py-1 grid sm:justify-end sm:absolute sm:top-3.5 sm:end-3.5">{cornerEl}</div>}
</CardHeader>
<div className="ps-0 w-[calc(100%-1.5em)] h-48 md:h-52 relative group">
<div className={cn("ps-0 w-[calc(100%-1.5em)] relative group", legend ? "h-54 md:h-56" : "h-48 md:h-52")}>
{
<Spinner
msg={empty ? t`Waiting for enough records to display` : undefined}

View File

@@ -0,0 +1,156 @@
import { t } from "@lingui/core/macro"
import { useStore } from "@nanostores/react"
import { MoreHorizontalIcon } from "lucide-react"
import { memo, useRef, useState } from "react"
import AreaChartDefault from "@/components/charts/area-chart"
import ChartTimeSelect from "@/components/charts/chart-time-select"
import { useNetworkInterfaces } from "@/components/charts/hooks"
import { Button } from "@/components/ui/button"
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
import { DialogTitle } from "@/components/ui/dialog"
import { $userSettings } from "@/lib/stores"
import { decimalString, formatBytes, toFixedFloat } from "@/lib/utils"
import type { ChartData } from "@/types"
import { ChartCard } from "../system"
export default memo(function NetworkSheet({
chartData,
dataEmpty,
grid,
maxValues,
}: {
chartData: ChartData
dataEmpty: boolean
grid: boolean
maxValues: boolean
}) {
const [netInterfacesOpen, setNetInterfacesOpen] = useState(false)
const userSettings = useStore($userSettings)
const netInterfaces = useNetworkInterfaces(chartData.systemStats.at(-1)?.stats?.ni ?? {})
const showNetLegend = netInterfaces.length > 0 && netInterfaces.length < 15
const hasOpened = useRef(false)
if (netInterfacesOpen && !hasOpened.current) {
hasOpened.current = true
}
if (!netInterfaces.length) {
return null
}
return (
<Sheet open={netInterfacesOpen} onOpenChange={setNetInterfacesOpen}>
<DialogTitle className="sr-only">{t`Network traffic of public interfaces`}</DialogTitle>
<SheetTrigger asChild>
<Button
title={t`View more`}
variant="outline"
size="icon"
className="shrink-0 max-sm:absolute max-sm:top-3 max-sm:end-3"
>
<MoreHorizontalIcon />
</Button>
</SheetTrigger>
{hasOpened.current && (
<SheetContent aria-describedby={undefined} className="overflow-auto w-200 !max-w-full p-4 sm:p-6">
<ChartTimeSelect className="w-[calc(100%-2em)]" />
<ChartCard
empty={dataEmpty}
grid={grid}
title={t`Download`}
description={t`Network traffic of public interfaces`}
legend={showNetLegend}
className="min-h-auto"
>
<AreaChartDefault
chartData={chartData}
maxToggled={maxValues}
itemSorter={(a, b) => b.value - a.value}
dataPoints={netInterfaces.data(1)}
legend={showNetLegend}
tickFormatter={(val) => {
const { value, unit } = formatBytes(val, true, userSettings.unitNet, false)
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
}}
contentFormatter={({ value }) => {
const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitNet, false)
return `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`
}}
/>
</ChartCard>
<ChartCard
empty={dataEmpty}
grid={grid}
title={t`Upload`}
description={t`Network traffic of public interfaces`}
legend={showNetLegend}
className="min-h-auto"
>
<AreaChartDefault
chartData={chartData}
maxToggled={maxValues}
itemSorter={(a, b) => b.value - a.value}
legend={showNetLegend}
dataPoints={netInterfaces.data(0)}
tickFormatter={(val) => {
const { value, unit } = formatBytes(val, true, userSettings.unitNet, false)
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
}}
contentFormatter={({ value }) => {
const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitNet, false)
return `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`
}}
/>
</ChartCard>
<ChartCard
empty={dataEmpty}
grid={grid}
title={t`Cumulative Download`}
description={t`Total data received for each interface`}
legend={showNetLegend}
className="min-h-auto"
>
<AreaChartDefault
chartData={chartData}
legend={showNetLegend}
dataPoints={netInterfaces.data(3)}
tickFormatter={(val) => {
const { value, unit } = formatBytes(val, false, userSettings.unitNet, false)
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
}}
contentFormatter={({ value }) => {
const { value: convertedValue, unit } = formatBytes(value, false, userSettings.unitNet, false)
return `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`
}}
/>
</ChartCard>
<ChartCard
empty={dataEmpty}
grid={grid}
title={t`Cumulative Upload`}
description={t`Total data sent for each interface`}
legend={showNetLegend}
className="min-h-auto"
>
<AreaChartDefault
chartData={chartData}
legend={showNetLegend}
dataPoints={netInterfaces.data(2)}
tickFormatter={(val) => {
const { value, unit } = formatBytes(val, false, userSettings.unitNet, false)
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
}}
contentFormatter={({ value }) => {
const { value: convertedValue, unit } = formatBytes(value, false, userSettings.unitNet, false)
return `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`
}}
/>
</ChartCard>
</SheetContent>
)}
</Sheet>
)
})

View File

@@ -1,5 +1,5 @@
import { cn } from "@/lib/utils"
import { LoaderCircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
export default function ({ msg, className }: { msg?: string; className?: string }) {
return (

View File

@@ -1,11 +1,18 @@
import { SystemRecord } from "@/types"
import { CellContext, ColumnDef, HeaderContext } from "@tanstack/react-table"
import { ClassValue } from "clsx"
import { t } from "@lingui/core/macro"
import { Trans, useLingui } from "@lingui/react/macro"
import { useStore } from "@nanostores/react"
import { getPagePath } from "@nanostores/router"
import type { CellContext, ColumnDef, HeaderContext } from "@tanstack/react-table"
import type { ClassValue } from "clsx"
import type { SystemRecord, SystemStats, SystemStatsRecord, SystemdService, SystemdStatsRecord } from "@/types"
import {
ArrowUpDownIcon,
ChevronRightSquareIcon,
CheckIcon,
CopyIcon,
CpuIcon,
HardDriveIcon,
ListChecks,
MemoryStickIcon,
MoreHorizontalIcon,
PauseCircleIcon,
@@ -15,7 +22,10 @@ import {
Trash2Icon,
WifiIcon,
} from "lucide-react"
import { Button } from "../ui/button"
import { memo, useMemo, useRef, useState } from "react"
import { isReadOnlyUser, pb } from "@/lib/api"
import { ConnectionType, connectionTypeLabels, MeterState, SystemStatus } from "@/lib/enums"
import { $longestSystemNameLen, $userSettings } from "@/lib/stores"
import {
cn,
copyToClipboard,
@@ -25,24 +35,11 @@ import {
getMeterState,
parseSemVer,
} from "@/lib/utils"
import { EthernetIcon, GpuIcon, HourglassIcon, ThermometerIcon } from "../ui/icons"
import { useStore } from "@nanostores/react"
import { $longestSystemNameLen, $userSettings } from "@/lib/stores"
import { Trans, useLingui } from "@lingui/react/macro"
import { useMemo, useRef, useState } from "react"
import { memo } from "react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "../ui/dropdown-menu"
import AlertButton from "../alerts/alert-button"
import { Dialog } from "../ui/dialog"
import { SystemDialog } from "../add-system"
import { AlertDialog } from "../ui/alert-dialog"
import AlertButton from "../alerts/alert-button"
import { $router, Link } from "../router"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
@@ -51,12 +48,16 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "../ui/alert-dialog"
import { buttonVariants } from "../ui/button"
import { t } from "@lingui/core/macro"
import { MeterState, SystemStatus } from "@/lib/enums"
import { $router, Link } from "../router"
import { getPagePath } from "@nanostores/router"
import { isReadOnlyUser, pb } from "@/lib/api"
import { Button, buttonVariants } from "../ui/button"
import { Dialog } from "../ui/dialog"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "../ui/dropdown-menu"
import { EthernetIcon, GpuIcon, HourglassIcon, ThermometerIcon, WebSocketIcon } from "../ui/icons"
const STATUS_COLORS = {
[SystemStatus.Up]: "bg-green-500",
@@ -273,24 +274,46 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
return null
}
const system = info.row.original
const color = {
"text-green-500": version === globalThis.BESZEL.HUB_VERSION,
"text-yellow-500": version !== globalThis.BESZEL.HUB_VERSION,
"text-red-500": system.status !== SystemStatus.Up,
}
return (
<span className={cn("flex gap-1.5 items-center md:pe-5 tabular-nums", viewMode === "table" && "ps-0.5")}>
<IndicatorDot
system={system}
className={
(system.status !== SystemStatus.Up && STATUS_COLORS[SystemStatus.Paused]) ||
(version === globalThis.BESZEL.HUB_VERSION && STATUS_COLORS[SystemStatus.Up]) ||
STATUS_COLORS[SystemStatus.Pending]
}
/>
<Link
href={getPagePath($router, "system", { name: system.name })}
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")} />}
<span className="truncate max-w-14">{info.getValue() as string}</span>
</span>
</Link>
)
},
},
{
id: "systemd",
name: () => t`Services`,
size: 50,
Icon: ListChecks,
hideSort: true,
header: sortableHeader,
cell: ({ row }) => <SystemdCell systemId={row.original.id} />,
},
{
id: "actions",
// @ts-ignore
// @ts-expect-error
name: () => t({ message: "Actions", comment: "Table column" }),
size: 50,
cell: ({ row }) => (
@@ -305,12 +328,13 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
function sortableHeader(context: HeaderContext<SystemRecord, unknown>) {
const { column } = context
// @ts-ignore
// @ts-expect-error
const { Icon, hideSort, name }: { Icon: React.ElementType; name: () => string; hideSort: boolean } = column.columnDef
const isSorted = column.getIsSorted()
return (
<Button
variant="ghost"
className="h-9 px-3 flex"
className={cn("h-9 px-3 flex duration-50", isSorted && "bg-accent/70 light:bg-accent text-accent-foreground/90")}
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
{Icon && <Icon className="me-2 size-4" />}
@@ -350,10 +374,50 @@ export function IndicatorDot({ system, className }: { system: SystemRecord; clas
)
}
const SystemdCell = ({ systemId }: { systemId: string }) => {
const [stats, setStats] = useState<SystemdService[] | null>(null);
useEffect(() => {
const fetchStats = async () => {
try {
const record = await pb.collection("systemd_stats").getFirstListItem<SystemdStatsRecord>(`system="${systemId}"`, {
sort: "-created",
});
setStats(record.stats);
} catch (error) {
// Handle case where no stats are found
setStats(null);
}
};
fetchStats();
}, [systemId]);
if (!stats) {
return <span className="text-muted-foreground">-</span>;
}
const failed = stats.filter(s => s.s === 'failed').length;
if (failed > 0) {
return (
<div className="tabular-nums text-red-500">
{failed}
</div>
);
}
return (
<div className="text-green-500 flex items-center justify-center">
<CheckIcon className="size-4" />
</div>
);
};
export const ActionsButton = memo(({ system }: { system: SystemRecord }) => {
const [deleteOpen, setDeleteOpen] = useState(false)
const [editOpen, setEditOpen] = useState(false)
let editOpened = useRef(false)
const editOpened = useRef(false)
const { t } = useLingui()
const { id, status, host, name } = system

View File

@@ -1,17 +1,31 @@
import { Trans, useLingui } from "@lingui/react/macro"
import { useStore } from "@nanostores/react"
import { getPagePath } from "@nanostores/router"
import {
ColumnDef,
ColumnFiltersState,
getFilteredRowModel,
SortingState,
getSortedRowModel,
type ColumnDef,
type ColumnFiltersState,
flexRender,
VisibilityState,
getCoreRowModel,
getFilteredRowModel,
getSortedRowModel,
type Row,
type SortingState,
type Table as TableType,
useReactTable,
Row,
Table as TableType,
type VisibilityState,
} from "@tanstack/react-table"
import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { useVirtualizer, type VirtualItem } from "@tanstack/react-virtual"
import {
ArrowDownIcon,
ArrowUpDownIcon,
ArrowUpIcon,
EyeIcon,
FilterIcon,
LayoutGridIcon,
LayoutListIcon,
Settings2Icon,
} from "lucide-react"
import { memo, useEffect, useMemo, useRef, useState } from "react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
@@ -24,30 +38,16 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { SystemRecord } from "@/types"
import {
ArrowUpDownIcon,
LayoutGridIcon,
LayoutListIcon,
ArrowDownIcon,
ArrowUpIcon,
Settings2Icon,
EyeIcon,
FilterIcon,
} from "lucide-react"
import { memo, useEffect, useMemo, useRef, useState } from "react"
import { $pausedSystems, $downSystems, $upSystems, $systems } from "@/lib/stores"
import { useStore } from "@nanostores/react"
import { cn, runOnce, useBrowserStorage } from "@/lib/utils"
import { $router, Link } from "../router"
import { useLingui, Trans } from "@lingui/react/macro"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
import { Input } from "@/components/ui/input"
import { getPagePath } from "@nanostores/router"
import SystemsTableColumns, { ActionsButton, IndicatorDot } from "./systems-table-columns"
import AlertButton from "../alerts/alert-button"
import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { SystemStatus } from "@/lib/enums"
import { useVirtualizer, VirtualItem } from "@tanstack/react-virtual"
import { $downSystems, $pausedSystems, $systems, $upSystems } from "@/lib/stores"
import { cn, runOnce, useBrowserStorage } from "@/lib/utils"
import type { SystemRecord } from "@/types"
import AlertButton from "../alerts/alert-button"
import { $router, Link } from "../router"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
import SystemsTableColumns, { ActionsButton, IndicatorDot } from "./systems-table-columns"
type ViewMode = "table" | "grid"
type StatusFilter = "all" | SystemRecord["status"]
@@ -309,128 +309,121 @@ export default function SystemsTable() {
)
}
const AllSystemsTable = memo(function ({
table,
rows,
colLength,
}: {
table: TableType<SystemRecord>
rows: Row<SystemRecord>[]
colLength: number
}) {
// The virtualizer will need a reference to the scrollable container element
const scrollRef = useRef<HTMLDivElement>(null)
const AllSystemsTable = memo(
({ table, rows, colLength }: { table: TableType<SystemRecord>; rows: Row<SystemRecord>[]; colLength: number }) => {
// The virtualizer will need a reference to the scrollable container element
const scrollRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({
count: rows.length,
estimateSize: () => (rows.length > 10 ? 56 : 60),
getScrollElement: () => scrollRef.current,
overscan: 5,
})
const virtualRows = virtualizer.getVirtualItems()
const virtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({
count: rows.length,
estimateSize: () => (rows.length > 10 ? 56 : 60),
getScrollElement: () => scrollRef.current,
overscan: 5,
})
const virtualRows = virtualizer.getVirtualItems()
const paddingTop = Math.max(0, virtualRows[0]?.start ?? 0 - virtualizer.options.scrollMargin)
const paddingBottom = Math.max(0, virtualizer.getTotalSize() - (virtualRows[virtualRows.length - 1]?.end ?? 0))
const paddingTop = Math.max(0, virtualRows[0]?.start ?? 0 - virtualizer.options.scrollMargin)
const paddingBottom = Math.max(0, virtualizer.getTotalSize() - (virtualRows[virtualRows.length - 1]?.end ?? 0))
return (
<div
className={cn(
"h-min max-h-[calc(100dvh-17rem)] max-w-full relative overflow-auto border rounded-md",
// don't set min height if there are less than 2 rows, do set if we need to display the empty state
(!rows.length || rows.length > 2) && "min-h-50"
)}
ref={scrollRef}
>
{/* add header height to table size */}
<div style={{ height: `${virtualizer.getTotalSize() + 50}px`, paddingTop, paddingBottom }}>
<table className="text-sm w-full h-full">
<SystemsTableHead table={table} colLength={colLength} />
<TableBody onMouseEnter={preloadSystemDetail}>
{rows.length ? (
virtualRows.map((virtualRow) => {
const row = rows[virtualRow.index] as Row<SystemRecord>
return (
<SystemTableRow
key={row.id}
row={row}
virtualRow={virtualRow}
length={rows.length}
colLength={colLength}
/>
)
})
) : (
<TableRow>
<TableCell colSpan={colLength} className="h-37 text-center pointer-events-none">
<Trans>No systems found.</Trans>
</TableCell>
</TableRow>
)}
</TableBody>
</table>
</div>
</div>
)
})
function SystemsTableHead({ table, colLength }: { table: TableType<SystemRecord>; colLength: number }) {
const { i18n } = useLingui()
return useMemo(() => {
return (
<TableHeader className="sticky top-0 z-20 w-full border-b-2">
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead className="px-1.5" key={header.id}>
{flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
)
})}
</tr>
))}
</TableHeader>
<div
className={cn(
"h-min max-h-[calc(100dvh-17rem)] max-w-full relative overflow-auto border rounded-md",
// don't set min height if there are less than 2 rows, do set if we need to display the empty state
(!rows.length || rows.length > 2) && "min-h-50"
)}
ref={scrollRef}
>
{/* add header height to table size */}
<div style={{ height: `${virtualizer.getTotalSize() + 50}px`, paddingTop, paddingBottom }}>
<table className="text-sm w-full h-full">
<SystemsTableHead table={table} />
<TableBody onMouseEnter={preloadSystemDetail}>
{rows.length ? (
virtualRows.map((virtualRow) => {
const row = rows[virtualRow.index] as Row<SystemRecord>
return (
<SystemTableRow
key={row.id}
row={row}
virtualRow={virtualRow}
length={rows.length}
colLength={colLength}
/>
)
})
) : (
<TableRow>
<TableCell colSpan={colLength} className="h-37 text-center pointer-events-none">
<Trans>No systems found.</Trans>
</TableCell>
</TableRow>
)}
</TableBody>
</table>
</div>
</div>
)
}, [i18n.locale, colLength])
}
)
function SystemsTableHead({ table }: { table: TableType<SystemRecord> }) {
const { t } = useLingui()
return (
<TableHeader className="sticky top-0 z-50 w-full border-b-2">
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead className="px-1.5" key={header.id}>
{flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
)
})}
</tr>
))}
</TableHeader>
)
}
const SystemTableRow = memo(function ({
row,
virtualRow,
colLength,
}: {
row: Row<SystemRecord>
virtualRow: VirtualItem
length: number
colLength: number
}) {
const system = row.original
const { t } = useLingui()
return useMemo(() => {
return (
<TableRow
// data-state={row.getIsSelected() && "selected"}
className={cn("cursor-pointer transition-opacity relative safari:transform-3d", {
"opacity-50": system.status === SystemStatus.Paused,
})}
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
style={{
width: cell.column.getSize(),
height: virtualRow.size,
}}
className="py-0"
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
)
}, [system, system.status, colLength, t])
})
const SystemTableRow = memo(
({
row,
virtualRow,
colLength,
}: {
row: Row<SystemRecord>
virtualRow: VirtualItem
length: number
colLength: number
}) => {
const system = row.original
const { t } = useLingui()
return useMemo(() => {
return (
<TableRow
// data-state={row.getIsSelected() && "selected"}
className={cn("cursor-pointer transition-opacity relative safari:transform-3d", {
"opacity-50": system.status === SystemStatus.Paused,
})}
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
style={{
width: cell.column.getSize(),
height: virtualRow.size,
}}
className="py-0"
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
)
}, [system, system.status, colLength, t])
}
)
const SystemCard = memo(
({ row, table, colLength }: { row: Row<SystemRecord>; table: TableType<SystemRecord>; colLength: number }) => {
@@ -471,7 +464,7 @@ const SystemCard = memo(
if (!column.getIsVisible() || column.id === "system" || column.id === "actions") return null
const cell = row.getAllCells().find((cell) => cell.column.id === column.id)
if (!cell) return null
// @ts-ignore
// @ts-expect-error
const { Icon, name } = column.columnDef as ColumnDef<SystemRecord, unknown>
return (
<>

View File

@@ -130,3 +130,12 @@ export function HourglassIcon(props: SVGProps<SVGSVGElement>) {
</svg>
)
}
export function WebSocketIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 256 193" {...props} fill="currentColor">
<title>WebSocket</title>
<path d="M192 145h32V68l-36-35-22 22 26 27zm32 16H113l-26-27 11-11 22 22h45l-44-45 11-11 44 44V88l-21-22 11-11-55-55H0l32 32h65l24 23-34 34-24-23V48H32v31l55 55-23 22 36 36h156z" />
</svg>
)
}

View File

@@ -53,3 +53,11 @@ export enum HourFormat {
"12h" = "12h",
"24h" = "24h",
}
/** Connection type */
export enum ConnectionType {
SSH = 1,
WebSocket,
}
export const connectionTypeLabels = ["", "SSH", "WebSocket"] as const

View File

@@ -1,6 +1,7 @@
import { t } from "@lingui/core/macro"
import { type ClassValue, clsx } from "clsx"
import { timeDay, timeHour } from "d3-time"
import { listenKeys } from "nanostores"
import { useEffect, useState } from "react"
import { twMerge } from "tailwind-merge"
import { prependBasePath } from "@/components/router"
@@ -8,7 +9,6 @@ import { toast } from "@/components/ui/use-toast"
import type { ChartTimeData, FingerprintRecord, SemVer, SystemRecord } from "@/types"
import { HourFormat, MeterState, Unit } from "./enums"
import { $copyContent, $userSettings } from "./stores"
import { listenKeys } from "nanostores"
export const FAVICON_DEFAULT = "favicon.svg"
export const FAVICON_GREEN = "favicon-green.svg"
@@ -179,8 +179,8 @@ export function formatTemperature(celsius: number, unit?: Unit): { value: number
if (!unit) {
unit = $userSettings.get().unitTemp || Unit.Celsius
}
// need loose equality check due to form data being strings
if (unit === Unit.Fahrenheit) {
// biome-ignore lint/suspicious/noDoubleEquals: need loose equality check due to form data being strings
if (unit == Unit.Fahrenheit) {
return {
value: celsius * 1.8 + 32,
unit: "°F",
@@ -202,8 +202,8 @@ export function formatBytes(
// Convert MB to bytes if isMegabytes is true
if (isMegabytes) size *= 1024 * 1024
// need loose equality check due to form data being strings
if (unit === Unit.Bits) {
// biome-ignore lint/suspicious/noDoubleEquals: need loose equality check due to form data being strings
if (unit == Unit.Bits) {
const bits = size * 8
const suffix = perSecond ? "ps" : ""
if (bits < 1000) return { value: bits, unit: `b${suffix}` }

View File

@@ -173,6 +173,10 @@ msgstr "متوسط استخدام وحدة المعالجة المركزية ع
msgid "Average utilization of {0}"
msgstr "متوسط ​​استخدام {0}"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "متوسط استغلال محركات GPU"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -363,6 +367,14 @@ msgstr "أنشئت"
msgid "Critical (%)"
msgstr "حرج (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "التنزيل التراكمي"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "الرفع التراكمي"
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -441,6 +453,10 @@ msgstr "معطل"
msgid "Down ({downSystemsLength})"
msgstr "معطل ({downSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr "تنزيل"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "المدة"
@@ -452,6 +468,7 @@ msgstr "تعديل"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "البريد الإشباكي"
@@ -472,6 +489,10 @@ msgstr "أدخل عنوان البريد الإشباكي لإعادة تعيي
msgid "Enter email address..."
msgstr "أدخل عنوان البريد الإشباكي..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "أدخل كلمة المرور لمرة واحدة الخاصة بك."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -542,6 +563,12 @@ msgstr "لمدة <0>{min}</0> {min, plural, one {دقيقة} other {دقائق}}
msgid "Forgot password?"
msgstr "هل نسيت كلمة المرور؟"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr "أمر FreeBSD"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -553,6 +580,10 @@ msgstr "ممتلئة"
msgid "General"
msgstr "عام"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "محركات GPU"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "استهلاك طاقة وحدة معالجة الرسوميات"
@@ -645,6 +676,7 @@ msgid "Manage display and notification preferences."
msgstr "إدارة تفضيلات العرض والإشعارات."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr "تعليمات الإعداد اليدوي"
@@ -680,6 +712,9 @@ msgid "Network traffic of docker containers"
msgstr "حركة مرور الشبكة لحاويات الدوكر"
#: 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
msgid "Network traffic of public interfaces"
msgstr "حركة مرور الشبكة للواجهات العامة"
@@ -715,6 +750,10 @@ msgstr "دعم OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "في كل إعادة تشغيل، سيتم تحديث الأنظمة في قاعدة البيانات لتتطابق مع الأنظمة المعرفة في الملف."
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "كلمة مرور لمرة واحدة"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -833,6 +872,14 @@ msgstr "قراءة"
msgid "Received"
msgstr "تم الاستلام"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "طلب كلمة مرور لمرة واحدة"
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr "طلب OTP"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "إعادة تعيين كلمة المرور"
@@ -888,10 +935,6 @@ msgstr "تم الإرسال"
msgid "Set percentage thresholds for meter colors."
msgstr "تعيين عتبات النسبة المئوية لألوان العداد."
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr "يحدد النطاق الزمني الافتراضي للرسوم البيانية عند عرض النظام."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1002,6 +1045,10 @@ msgstr "معدل نقل {extraFsName}"
msgid "Throughput of root filesystem"
msgstr "معدل نقل نظام الملفات الجذر"
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr "تنسيق الوقت"
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "إلى البريد الإشباكي"
@@ -1034,6 +1081,14 @@ msgstr "تسمح الرموز المميزة للوكلاء بالاتصال و
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "تُستخدم الرموز المميزة والبصمات للمصادقة على اتصالات WebSocket إلى المحور."
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "إجمالي البيانات المستلمة لكل واجهة"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr "إجمالي البيانات المرسلة لكل واجهة"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "يتم التفعيل عندما يتجاوز متوسط التحميل لمدة دقيقة واحدة عتبة معينة"
@@ -1048,7 +1103,7 @@ msgstr "يتم التفعيل عندما يتجاوز متوسط التحميل
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "يتم التفعيل عندما <EFBFBD><EFBFBD>تجاوز أي مستشعر عتبة معينة"
msgstr "يتم التفعيل عندما يتجاوز أي مستشعر عتبة معينة"
#: src/lib/alerts.ts
msgid "Triggers when combined up/down exceeds a threshold"
@@ -1095,6 +1150,10 @@ msgstr "قيد التشغيل"
msgid "Up ({upSystemsLength})"
msgstr "قيد التشغيل ({upSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "رفع"
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr "مدة التشغيل"
@@ -1128,6 +1187,10 @@ msgstr "القيمة"
msgid "View"
msgstr "عرض"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "عرض المزيد"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr "عرض أحدث 200 تنبيه."

View File

@@ -173,6 +173,10 @@ msgstr "Средно използване на процесора на цяла
msgid "Average utilization of {0}"
msgstr "Средно използване на {0}"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "Средно използване на GPU двигатели"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -363,6 +367,14 @@ msgstr "Създаден"
msgid "Critical (%)"
msgstr "Критично (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "Кумулативно изтегляне"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "Кумулативно качване"
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -441,6 +453,10 @@ msgstr "Офлайн"
msgid "Down ({downSystemsLength})"
msgstr "Офлайн ({downSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr "Изтегляне"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "Продължителност"
@@ -452,6 +468,7 @@ msgstr "Редактирай"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "Имейл"
@@ -472,6 +489,10 @@ msgstr "Въведи имейл адрес за да нулираш парола
msgid "Enter email address..."
msgstr "Въведи имейл адрес..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "Въведете Вашата еднократна парола."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -542,6 +563,12 @@ msgstr "За <0>{min}</0> {min, plural, one {минута} other {минути}}
msgid "Forgot password?"
msgstr "Забравена парола?"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr "FreeBSD команда"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -553,6 +580,10 @@ msgstr "Пълна"
msgid "General"
msgstr "Общо"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "GPU двигатели"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "Консумация на ток от графична карта"
@@ -645,6 +676,7 @@ msgid "Manage display and notification preferences."
msgstr "Управление на предпочитанията за показване и уведомяване."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr "Инструкции за ръчна настройка"
@@ -680,6 +712,9 @@ msgid "Network traffic of docker containers"
msgstr "Мрежов трафик на docker контейнери"
#: 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
msgid "Network traffic of public interfaces"
msgstr "Мрежов трафик на публични интерфейси"
@@ -715,6 +750,10 @@ msgstr "Поддръжка на OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "На всеки рестарт, системите в датабазата ще бъдат обновени да съвпадат със системите зададени във файла."
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Еднократна парола"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -833,6 +872,14 @@ msgstr "Прочети"
msgid "Received"
msgstr "Получени"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "Заявка за еднократна парола"
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr "Заявка OTP"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "Нулиране на парола"
@@ -888,10 +935,6 @@ msgstr "Изпратени"
msgid "Set percentage thresholds for meter colors."
msgstr "Задайте процентни прагове за цветовете на измервателните уреди."
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr "Задава диапазона за време за диаграмите, когато се разглежда система."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1002,6 +1045,10 @@ msgstr "Пропускателна способност на {extraFsName}"
msgid "Throughput of root filesystem"
msgstr "Пропускателна способност на root файловата система"
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr "Формат на времето"
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "До имейл(ите)"
@@ -1034,6 +1081,14 @@ msgstr "Токените позволяват на агентите да се с
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Токените и пръстовите отпечатъци се използват за удостоверяване на WebSocket връзките към концентратора."
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Общо получени данни за всеки интерфейс"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr "Общо изпратени данни за всеки интерфейс"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "Задейства се, когато употребата на паметта за 1 минута надвиши зададен праг"
@@ -1095,6 +1150,10 @@ msgstr "Нагоре"
msgid "Up ({upSystemsLength})"
msgstr "Нагоре ({upSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "Качване"
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr "Време на работа"
@@ -1128,6 +1187,10 @@ msgstr "Стойност"
msgid "View"
msgstr "Изглед"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Виж повече"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr "Прегледайте последните си 200 сигнала."

View File

@@ -173,6 +173,10 @@ msgstr "Průměrné využití CPU v celém systému"
msgid "Average utilization of {0}"
msgstr "Průměrné využití {0}"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "Průměrné využití GPU engine"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -363,6 +367,14 @@ msgstr "Vytvořeno"
msgid "Critical (%)"
msgstr "Kritické (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "Kumulativní stažení"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "Kumulativní odeslání"
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -441,6 +453,10 @@ msgstr "Nefunkční"
msgid "Down ({downSystemsLength})"
msgstr "Nefunkční ({downSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr "Stažení"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "Doba trvání"
@@ -452,6 +468,7 @@ msgstr "Upravit"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "Email"
@@ -472,6 +489,10 @@ msgstr "Zadejte e-mailovou adresu pro obnovu hesla"
msgid "Enter email address..."
msgstr "Zadejte e-mailovou adresu..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "Zadejte Vaše jednorázové heslo."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -542,6 +563,12 @@ msgstr "Za <0>{min}</0> {min, plural, one {minutu} few {minuty} other {minut}}"
msgid "Forgot password?"
msgstr "Zapomněli jste heslo?"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr "FreeBSD příkaz"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -553,6 +580,10 @@ msgstr "Plná"
msgid "General"
msgstr "Obecné"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "GPU enginy"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "Spotřeba energie GPU"
@@ -645,6 +676,7 @@ msgid "Manage display and notification preferences."
msgstr "Správa nastavení zobrazení a oznámení."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr "Pokyny k manuálnímu nastavení"
@@ -680,6 +712,9 @@ msgid "Network traffic of docker containers"
msgstr "Síťový provoz kontejnerů docker"
#: 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
msgid "Network traffic of public interfaces"
msgstr "Síťový provoz veřejných rozhraní"
@@ -715,6 +750,10 @@ msgstr "Podpora OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "Při každém restartu budou systémy v databázi aktualizovány tak, aby odpovídaly systémům definovaným v souboru."
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Jednorázové heslo"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -833,6 +872,14 @@ msgstr "Číst"
msgid "Received"
msgstr "Přijato"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "Požádat o jednorázové heslo"
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr "Požádat OTP"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "Obnovit heslo"
@@ -888,10 +935,6 @@ msgstr "Odeslat"
msgid "Set percentage thresholds for meter colors."
msgstr "Nastavte procentuální prahové hodnoty pro barvy měřičů."
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr "Nastaví výchozí časový rozsah grafů, když je systém zobrazen."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1002,6 +1045,10 @@ msgstr "Propustnost {extraFsName}"
msgid "Throughput of root filesystem"
msgstr "Propustnost kořenového souborového systému"
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr "Formát času"
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "Na email(y)"
@@ -1034,6 +1081,14 @@ msgstr "Tokeny umožňují agentům připojení a registraci. Otisky jsou stabil
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Tokeny a otisky slouží k ověření připojení WebSocket k uzlu."
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Celkový přijatý objem dat pro každé rozhraní"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr "Celkový odeslaný objem dat pro každé rozhraní"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "Spustí se, když využití paměti během 1 minuty překročí prahovou hodnotu"
@@ -1095,6 +1150,10 @@ msgstr "Funkční"
msgid "Up ({upSystemsLength})"
msgstr "Funkční ({upSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "Odeslání"
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr "Doba provozu"
@@ -1128,6 +1187,10 @@ msgstr "Hodnota"
msgid "View"
msgstr "Zobrazení"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Zobrazit více"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr "Zobrazit vašich 200 nejnovějších upozornění."

View File

@@ -173,6 +173,10 @@ msgstr "Gennemsnitlig systembaseret CPU-udnyttelse"
msgid "Average utilization of {0}"
msgstr "Gennemsnitlig udnyttelse af {0}"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "Gennemsnitlig udnyttelse af GPU-motorer"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -363,6 +367,14 @@ msgstr ""
msgid "Critical (%)"
msgstr "Kritisk (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "Kumulativ download"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "Kumulativ upload"
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -441,6 +453,10 @@ msgstr "Nede"
msgid "Down ({downSystemsLength})"
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr "Download"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr ""
@@ -452,6 +468,7 @@ msgstr "Rediger"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "E-mail"
@@ -472,6 +489,10 @@ msgstr "Indtast e-mailadresse for at nulstille adgangskoden"
msgid "Enter email address..."
msgstr "Indtast e-mailadresse..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "Indtast din engangsadgangskode."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -542,6 +563,12 @@ msgstr "For <0>{min}</0> {min, plural, one {minut} other {minutter}}"
msgid "Forgot password?"
msgstr "Glemt adgangskode?"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr "FreeBSD kommando"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -553,6 +580,10 @@ msgstr "Fuldt opladt"
msgid "General"
msgstr "Generelt"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "GPU-motorer"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "Gpu Strøm Træk"
@@ -645,6 +676,7 @@ msgid "Manage display and notification preferences."
msgstr "Administrer display og notifikationsindstillinger."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr "Manuel opsætningsvejledning"
@@ -680,6 +712,9 @@ msgid "Network traffic of docker containers"
msgstr "Netværkstrafik af dockercontainere"
#: 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
msgid "Network traffic of public interfaces"
msgstr "Netværkstrafik af offentlige grænseflader"
@@ -715,6 +750,10 @@ msgstr "OAuth 2 / OIDC understøttelse"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "Ved hver genstart vil systemer i databasen blive opdateret til at matche de systemer, der er defineret i filen."
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Engangsadgangskode"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -833,6 +872,14 @@ msgstr "Læs"
msgid "Received"
msgstr "Modtaget"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "Anmod om engangsadgangskode"
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr "Anmod OTP"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "Nulstil adgangskode"
@@ -888,10 +935,6 @@ msgstr "Sendt"
msgid "Set percentage thresholds for meter colors."
msgstr "Indstil procentvise tærskler for målerfarver."
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr "Sætter standardtidsintervallet for diagrammer når et system vises."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1002,6 +1045,10 @@ msgstr "Gennemløb af {extraFsName}"
msgid "Throughput of root filesystem"
msgstr "Gennemløb af rodfilsystemet"
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr "Tidsformat"
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "Til email(s)"
@@ -1034,6 +1081,14 @@ msgstr ""
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Samlet modtaget data for hver interface"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr "Samlet sendt data for hver interface"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr ""
@@ -1095,6 +1150,10 @@ msgstr "Oppe"
msgid "Up ({upSystemsLength})"
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "Upload"
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr "Oppetid"
@@ -1128,6 +1187,10 @@ msgstr ""
msgid "View"
msgstr "Vis"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Se mere"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr ""

View File

@@ -173,6 +173,10 @@ msgstr "Durchschnittliche systemweite CPU-Auslastung"
msgid "Average utilization of {0}"
msgstr "Durchschnittliche Auslastung von {0}"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "Durchschnittliche Auslastung der GPU-Engines"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -363,6 +367,14 @@ msgstr "Erstellt"
msgid "Critical (%)"
msgstr "Kritisch (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "Kumulativer Download"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "Kumulativer Upload"
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -441,6 +453,10 @@ msgstr "Offline"
msgid "Down ({downSystemsLength})"
msgstr "Offline ({downSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr "Download"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "Dauer"
@@ -452,6 +468,7 @@ msgstr "Bearbeiten"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "E-Mail"
@@ -472,6 +489,10 @@ msgstr "E-Mail-Adresse eingeben, um das Passwort zurückzusetzen"
msgid "Enter email address..."
msgstr "E-Mail-Adresse eingeben..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "Geben Sie Ihr Einmalpasswort ein."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -542,6 +563,12 @@ msgstr "Für <0>{min}</0> {min, plural, one {Minute} other {Minuten}}"
msgid "Forgot password?"
msgstr "Passwort vergessen?"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr "FreeBSD Befehl"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -553,6 +580,10 @@ msgstr "Voll"
msgid "General"
msgstr "Allgemein"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "GPU-Engines"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "GPU-Leistungsaufnahme"
@@ -645,6 +676,7 @@ msgid "Manage display and notification preferences."
msgstr "Anzeige- und Benachrichtigungseinstellungen verwalten."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr "Anleitung zur manuellen Einrichtung"
@@ -680,6 +712,9 @@ msgid "Network traffic of docker containers"
msgstr "Netzwerkverkehr der Docker-Container"
#: 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
msgid "Network traffic of public interfaces"
msgstr "Netzwerkverkehr der öffentlichen Schnittstellen"
@@ -715,6 +750,10 @@ msgstr "OAuth 2 / OIDC-Unterstützung"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "Bei jedem Neustart werden die Systeme in der Datenbank aktualisiert, um den in der Datei definierten Systemen zu entsprechen."
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Einmalpasswort"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -833,6 +872,14 @@ msgstr "Lesen"
msgid "Received"
msgstr "Empfangen"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "Einmalpasswort anfordern"
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr "OTP anfordern"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "Passwort zurücksetzen"
@@ -888,10 +935,6 @@ msgstr "Gesendet"
msgid "Set percentage thresholds for meter colors."
msgstr "Prozentuale Schwellenwerte für Zählerfarben festlegen."
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr "Legt den Standardzeitraum für Diagramme fest, wenn ein System angezeigt wird."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1002,6 +1045,10 @@ msgstr "Durchsatz von {extraFsName}"
msgid "Throughput of root filesystem"
msgstr "Durchsatz des Root-Dateisystems"
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr "Zeitformat"
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "An E-Mail(s)"
@@ -1034,6 +1081,14 @@ msgstr "Tokens ermöglichen es Agents, sich zu verbinden und zu registrieren. Fi
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Tokens und Fingerabdrücke werden verwendet, um WebSocket-Verbindungen zum Hub zu authentifizieren."
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Gesamtdatenmenge für jede Schnittstelle empfangen"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr "Gesamtdatenmenge für jede Schnittstelle gesendet"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "Löst aus, wenn der Lastdurchschnitt der letzten Minute einen Schwellenwert überschreitet"
@@ -1095,6 +1150,10 @@ msgstr "aktiv"
msgid "Up ({upSystemsLength})"
msgstr "aktiv ({upSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "Upload"
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr "Betriebszeit"
@@ -1128,6 +1187,10 @@ msgstr "Wert"
msgid "View"
msgstr "Ansicht"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Mehr anzeigen"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr "Sieh dir die neusten 200 Alarme an."

View File

@@ -75,6 +75,10 @@ msgstr "5 min"
msgid "Actions"
msgstr "Actions"
#: src/components/charts/systemd-services-table.tsx
msgid "active"
msgstr "active"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Active"
@@ -168,6 +172,10 @@ msgstr "Average system-wide CPU utilization"
msgid "Average utilization of {0}"
msgstr "Average utilization of {0}"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "Average utilization of GPU engines"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -339,6 +347,7 @@ msgstr "Copy YAML"
msgid "CPU"
msgstr "CPU"
#: src/components/charts/systemd-services-table.tsx
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
#: src/lib/alerts.ts
@@ -358,6 +367,14 @@ msgstr "Created"
msgid "Critical (%)"
msgstr "Critical (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "Cumulative Download"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "Cumulative Upload"
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -436,6 +453,10 @@ msgstr "Down"
msgid "Down ({downSystemsLength})"
msgstr "Down ({downSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr "Download"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "Duration"
@@ -447,6 +468,7 @@ msgstr "Edit"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "Email"
@@ -467,6 +489,10 @@ msgstr "Enter email address to reset password"
msgid "Enter email address..."
msgstr "Enter email address..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "Enter your one-time password."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -502,6 +528,10 @@ msgstr "Export your current systems configuration."
msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)"
#: src/components/charts/systemd-services-table.tsx
msgid "failed"
msgstr "failed"
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "Failed to authenticate"
@@ -537,6 +567,12 @@ msgstr "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
msgid "Forgot password?"
msgstr "Forgot password?"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr "FreeBSD command"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -548,6 +584,10 @@ msgstr "Full"
msgid "General"
msgstr "General"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "GPU Engines"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "GPU Power Draw"
@@ -640,6 +680,7 @@ msgid "Manage display and notification preferences."
msgstr "Manage display and notification preferences."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr "Manual setup instructions"
@@ -648,6 +689,7 @@ msgstr "Manual setup instructions"
msgid "Max 1 min"
msgstr "Max 1 min"
#: src/components/charts/systemd-services-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Memory"
msgstr "Memory"
@@ -675,6 +717,9 @@ msgid "Network traffic of docker containers"
msgstr "Network traffic of docker containers"
#: 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
msgid "Network traffic of public interfaces"
msgstr "Network traffic of public interfaces"
@@ -683,6 +728,10 @@ msgstr "Network traffic of public interfaces"
msgid "Network unit"
msgstr "Network unit"
#: src/components/charts/systemd-services-table.tsx
msgid "No failed services."
msgstr "No failed services."
#: src/components/command-palette.tsx
msgid "No results found."
msgstr "No results found."
@@ -710,6 +759,10 @@ msgstr "OAuth 2 / OIDC support"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "On each restart, systems in the database will be updated to match the systems defined in the file."
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "One-time password"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -828,6 +881,14 @@ msgstr "Read"
msgid "Received"
msgstr "Received"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "Request a one-time password"
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr "Request OTP"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "Reset Password"
@@ -879,14 +940,18 @@ msgstr "See <0>notification settings</0> to configure how you receive alerts."
msgid "Sent"
msgstr "Sent"
#: src/components/charts/systemd-services-table.tsx
msgid "Service"
msgstr "Service"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Services"
msgstr "Services"
#: src/components/routes/settings/general.tsx
msgid "Set percentage thresholds for meter colors."
msgstr "Set percentage thresholds for meter colors."
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr "Sets the default time range for charts when a system is viewed."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -898,6 +963,14 @@ msgstr "Settings"
msgid "Settings saved"
msgstr "Settings saved"
#: src/components/charts/systemd-services-table.tsx
msgid "Show all"
msgstr "Show all"
#: src/components/charts/systemd-services-table.tsx
msgid "Show less"
msgstr "Show less"
#: src/components/login/auth-form.tsx
msgid "Sign in"
msgstr "Sign in"
@@ -915,6 +988,7 @@ msgstr "Sort By"
msgid "State"
msgstr "State"
#: src/components/charts/systemd-services-table.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
@@ -939,6 +1013,11 @@ msgstr "System"
msgid "System load averages over time"
msgstr "System load averages over time"
#: src/components/charts/systemd-services-table.tsx
#: src/components/routes/system.tsx
msgid "Systemd Services"
msgstr "Systemd Services"
#: src/components/navbar.tsx
msgid "Systems"
msgstr "Systems"
@@ -997,6 +1076,10 @@ msgstr "Throughput of {extraFsName}"
msgid "Throughput of root filesystem"
msgstr "Throughput of root filesystem"
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr "Time format"
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "To email(s)"
@@ -1029,6 +1112,14 @@ msgstr "Tokens allow agents to connect and register. Fingerprints are stable ide
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Total data received for each interface"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr "Total data sent for each interface"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "Triggers when 1 minute load average exceeds a threshold"
@@ -1090,6 +1181,10 @@ msgstr "Up"
msgid "Up ({upSystemsLength})"
msgstr "Up ({upSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "Upload"
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr "Uptime"
@@ -1123,6 +1218,10 @@ msgstr "Value"
msgid "View"
msgstr "View"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "View more"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr "View your 200 most recent alerts."

View File

@@ -173,6 +173,10 @@ msgstr "Utilización promedio de CPU del sistema"
msgid "Average utilization of {0}"
msgstr "Uso promedio de {0}"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "Utilización promedio de motores GPU"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -363,6 +367,14 @@ msgstr "Creado"
msgid "Critical (%)"
msgstr "Crítico (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "Descarga acumulativa"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "Carga acumulativa"
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -441,6 +453,10 @@ msgstr "Abajo"
msgid "Down ({downSystemsLength})"
msgstr "Abajo ({downSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr "Descargar"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "Duración"
@@ -452,6 +468,7 @@ msgstr "Editar"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "Correo electrónico"
@@ -472,6 +489,10 @@ msgstr "Ingrese la dirección de correo electrónico para restablecer la contras
msgid "Enter email address..."
msgstr "Ingrese dirección de correo..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "Ingrese su contraseña de un solo uso."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -542,6 +563,12 @@ msgstr "Por <0>{min}</0> {min, plural, one {minuto} other {minutos}}"
msgid "Forgot password?"
msgstr "¿Olvidó su contraseña?"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr "Comando FreeBSD"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -553,6 +580,10 @@ msgstr "Llena"
msgid "General"
msgstr "General"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "Motores GPU"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "Consumo de energía de la GPU"
@@ -645,6 +676,7 @@ msgid "Manage display and notification preferences."
msgstr "Administrar preferencias de visualización y notificaciones."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr "Instrucciones manuales de configuración"
@@ -680,6 +712,9 @@ msgid "Network traffic of docker containers"
msgstr "Tráfico de red de los contenedores de Docker"
#: 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
msgid "Network traffic of public interfaces"
msgstr "Tráfico de red de interfaces públicas"
@@ -715,6 +750,10 @@ msgstr "Soporte para OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "En cada reinicio, los sistemas en la base de datos se actualizarán para coincidir con los sistemas definidos en el archivo."
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Contraseña de un solo uso"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -833,6 +872,14 @@ msgstr "Lectura"
msgid "Received"
msgstr "Recibido"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "Solicitar contraseña de un solo uso"
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr "Solicitar OTP"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "Restablecer Contraseña"
@@ -888,10 +935,6 @@ msgstr "Enviado"
msgid "Set percentage thresholds for meter colors."
msgstr "Establecer umbrales de porcentaje para los colores de los medidores."
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr "Establece el rango de tiempo predeterminado para los gráficos cuando se visualiza un sistema."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1002,6 +1045,10 @@ msgstr "Rendimiento de {extraFsName}"
msgid "Throughput of root filesystem"
msgstr "Rendimiento del sistema de archivos raíz"
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr "Formato de hora"
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "A correo(s)"
@@ -1034,6 +1081,14 @@ msgstr "Los tokens permiten que los agentes se conecten y registren. Las huellas
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Los tokens y las huellas digitales se utilizan para autenticar las conexiones WebSocket al hub."
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Datos totales recibidos por cada interfaz"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr "Datos totales enviados por cada interfaz"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "Se activa cuando la carga media de 1 minuto supera un umbral"
@@ -1095,6 +1150,10 @@ msgstr "Activo"
msgid "Up ({upSystemsLength})"
msgstr "Activo ({upSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "Cargar"
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr "Tiempo de actividad"
@@ -1128,6 +1187,10 @@ msgstr "Valor"
msgid "View"
msgstr "Vista"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Ver más"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr "Ver sus 200 alertas más recientes."

View File

@@ -173,6 +173,10 @@ msgstr "میانگین استفاده از CPU در کل سیستم"
msgid "Average utilization of {0}"
msgstr "میانگین استفاده از {0}"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "میانگین استفاده از موتورهای GPU"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -363,6 +367,14 @@ msgstr "ایجاد شده"
msgid "Critical (%)"
msgstr "بحرانی (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "دانلود تجمعی"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "آپلود تجمعی"
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -441,6 +453,10 @@ msgstr "قطع"
msgid "Down ({downSystemsLength})"
msgstr "قطع ({downSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr "دانلود"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "مدت زمان"
@@ -452,6 +468,7 @@ msgstr "ویرایش"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "ایمیل"
@@ -472,6 +489,10 @@ msgstr "آدرس ایمیل را برای بازنشانی رمز عبور وا
msgid "Enter email address..."
msgstr "آدرس ایمیل را وارد کنید..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "رمز عبور یک‌بار مصرف خود را وارد کنید."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -542,6 +563,12 @@ msgstr "برای <0>{min}</0> {min, plural, one {دقیقه} other {دقیقه}}
msgid "Forgot password?"
msgstr "رمز عبور را فراموش کرده‌اید؟"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr "دستور FreeBSD"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -553,6 +580,10 @@ msgstr "پر"
msgid "General"
msgstr "عمومی"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "موتورهای GPU"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "مصرف برق پردازنده گرافیکی"
@@ -645,6 +676,7 @@ msgid "Manage display and notification preferences."
msgstr "مدیریت تنظیمات نمایش و اعلان‌ها."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr "دستورالعمل‌های راه‌اندازی دستی"
@@ -680,6 +712,9 @@ msgid "Network traffic of docker containers"
msgstr "ترافیک شبکه کانتینرهای داکر"
#: 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
msgid "Network traffic of public interfaces"
msgstr "ترافیک شبکه رابط‌های عمومی"
@@ -715,6 +750,10 @@ msgstr "پشتیبانی از OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "در هر بار راه‌اندازی مجدد، سیستم‌های موجود در پایگاه داده با سیستم‌های تعریف شده در فایل مطابقت داده می‌شوند."
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "رمز عبور یک‌بار مصرف"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -833,6 +872,14 @@ msgstr "خواندن"
msgid "Received"
msgstr "دریافت شد"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "درخواست رمز عبور یک‌بار مصرف"
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr "درخواست OTP"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "بازنشانی رمز عبور"
@@ -888,10 +935,6 @@ msgstr "ارسال شد"
msgid "Set percentage thresholds for meter colors."
msgstr "آستانه های درصدی را برای رنگ های متر تنظیم کنید."
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr "بازه زمانی پیش‌فرض برای نمودارها هنگام مشاهده یک سیستم را تعیین می‌کند."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1002,6 +1045,10 @@ msgstr "توان عملیاتی {extraFsName}"
msgid "Throughput of root filesystem"
msgstr "توان عملیاتی سیستم فایل ریشه"
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr "فرمت زمان"
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "به ایمیل(ها)"
@@ -1034,6 +1081,14 @@ msgstr "توکن‌ها به عامل‌ها اجازه اتصال و ثبت‌
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "توکن‌ها و اثرات انگشت برای احراز هویت اتصالات WebSocket به هاب استفاده می‌شوند."
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "داده‌های کل دریافت شده برای هر رابط"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr "داده‌های کل ارسال شده برای هر رابط"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "هنگامی که میانگین بار ۱ دقیقه‌ای از یک آستانه فراتر رود، فعال می‌شود"
@@ -1095,6 +1150,10 @@ msgstr "فعال"
msgid "Up ({upSystemsLength})"
msgstr "فعال ({upSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "آپلود"
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr "آپتایم"
@@ -1128,6 +1187,10 @@ msgstr "مقدار"
msgid "View"
msgstr "مشاهده"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "مشاهده بیشتر"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr "۲۰۰ هشدار اخیر خود را مشاهده کنید."

View File

@@ -173,6 +173,10 @@ msgstr "Utilisation moyenne du CPU à l'échelle du système"
msgid "Average utilization of {0}"
msgstr "Utilisation moyenne de {0}"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "Utilisation moyenne des moteurs GPU"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -363,6 +367,14 @@ msgstr ""
msgid "Critical (%)"
msgstr "Critique (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "Téléchargement cumulatif"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "Téléversement cumulatif"
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -441,6 +453,10 @@ msgstr "Injoignable"
msgid "Down ({downSystemsLength})"
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr "Télécharger"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr ""
@@ -452,6 +468,7 @@ msgstr "Éditer"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "Email"
@@ -472,6 +489,10 @@ msgstr "Entrez l'adresse email pour réinitialiser le mot de passe"
msgid "Enter email address..."
msgstr "Entrez l'adresse email..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "Entrez votre mot de passe à usage unique."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -542,6 +563,12 @@ msgstr "Pour <0>{min}</0> {min, plural, one {minute} other {minutes}}"
msgid "Forgot password?"
msgstr "Mot de passe oublié ?"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr "Commande FreeBSD"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -553,6 +580,10 @@ msgstr "Pleine"
msgid "General"
msgstr "Général"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "Moteurs GPU"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "Consommation du GPU"
@@ -645,6 +676,7 @@ msgid "Manage display and notification preferences."
msgstr "Gérer les préférences d'affichage et de notification."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr "Guide pour une installation manuelle"
@@ -680,6 +712,9 @@ msgid "Network traffic of docker containers"
msgstr "Trafic réseau des conteneurs Docker"
#: 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
msgid "Network traffic of public interfaces"
msgstr "Trafic réseau des interfaces publiques"
@@ -715,6 +750,10 @@ msgstr "Support OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "À chaque redémarrage, les systèmes dans la base de données seront mis à jour pour correspondre aux systèmes définis dans le fichier."
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Mot de passe à usage unique"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -833,6 +872,14 @@ msgstr "Lecture"
msgid "Received"
msgstr "Reçu"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "Demander un mot de passe à usage unique"
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr "Demander OTP"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "Réinitialiser le mot de passe"
@@ -888,10 +935,6 @@ msgstr "Envoyé"
msgid "Set percentage thresholds for meter colors."
msgstr "Définir des seuils de pourcentage pour les couleurs des compteurs."
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr "Définit la plage de temps par défaut pour les graphiques lorsqu'un système est consulté."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1002,6 +1045,10 @@ msgstr "Débit de {extraFsName}"
msgid "Throughput of root filesystem"
msgstr "Débit du système de fichiers racine"
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr "Format d'heure"
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "Aux email(s)"
@@ -1034,6 +1081,14 @@ msgstr "Les tokens permettent aux agents de se connecter et de s'enregistrer. Le
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Les tokens et les empreintes sont utilisés pour authentifier les connexions WebSocket vers le hub."
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Données totales reçues pour chaque interface"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr "Données totales envoyées pour chaque interface"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr ""
@@ -1095,6 +1150,10 @@ msgstr "Joignable"
msgid "Up ({upSystemsLength})"
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "Téléverser"
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr "Temps de fonctionnement"
@@ -1128,6 +1187,10 @@ msgstr ""
msgid "View"
msgstr "Vue"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Voir plus"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr ""

View File

@@ -8,15 +8,15 @@ msgstr ""
"Language: hr\n"
"Project-Id-Version: beszel\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"
"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"
"X-Crowdin-Project: beszel\n"
"X-Crowdin-Project-ID: 733311\n"
"X-Crowdin-Language: hr\n"
"X-Crowdin-File: /main/beszel/site/src/locales/en/en.po\n"
"X-Crowdin-File-ID: 16\n"
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
"X-Crowdin-File-ID: 32\n"
#. placeholder {0}: Math.trunc(system.info?.u / 86400)
#: src/components/routes/system.tsx
@@ -46,7 +46,7 @@ msgstr "1 sat"
#. Load average
#: src/components/charts/load-average-chart.tsx
msgid "1 min"
msgstr ""
msgstr "1 minut"
#: src/lib/utils.ts
msgid "1 week"
@@ -59,7 +59,7 @@ msgstr "12 sati"
#. Load average
#: src/components/charts/load-average-chart.tsx
msgid "15 min"
msgstr ""
msgstr "15 minuta"
#: src/lib/utils.ts
msgid "24 hours"
@@ -83,7 +83,7 @@ msgstr "Akcije"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Active"
msgstr ""
msgstr "Aktivan"
#: src/components/routes/home.tsx
msgid "Active Alerts"
@@ -141,7 +141,7 @@ msgstr "Jeste li sigurni da želite izbrisati {name}?"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Are you sure?"
msgstr ""
msgstr "Jeste li sigurni?"
#: src/components/copy-to-clipboard.tsx
msgid "Automatic copy requires a secure context."
@@ -173,6 +173,10 @@ msgstr "Prosječna iskorištenost procesora na cijelom sustavu"
msgid "Average utilization of {0}"
msgstr ""
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "Prosječna iskorištenost GPU motora"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -309,7 +313,7 @@ msgstr "Kopiraj docker run"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Environment variables"
msgid "Copy env"
msgstr ""
msgstr "Kopiraj env"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Copy host"
@@ -357,12 +361,20 @@ msgstr "Napravite račun"
#. Context: date created
#: src/components/alerts-history-columns.tsx
msgid "Created"
msgstr ""
msgstr "Kreiran"
#: src/components/routes/settings/general.tsx
msgid "Critical (%)"
msgstr "Kritično (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "Kumulativno preuzimanje"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "Kumulativno otpremanje"
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -441,17 +453,22 @@ msgstr ""
msgid "Down ({downSystemsLength})"
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr "Preuzmi"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr ""
msgstr "Trajanje"
#: src/components/add-system.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Edit"
msgstr ""
msgstr "Uredi"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "Email"
@@ -472,6 +489,10 @@ msgstr "Unesite email adresu za resetiranje lozinke"
msgid "Enter email address..."
msgstr "Unesite email adresu..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "Unesite Vašu jednokratnu lozinku."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -493,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
msgid "Export"
msgstr ""
msgstr "Izvezi"
#: src/components/routes/settings/config-yaml.tsx
msgid "Export configuration"
@@ -528,11 +549,11 @@ msgstr "Ažuriranje upozorenja nije uspjelo"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Filter..."
msgstr "Filter..."
msgstr "Filtriraj..."
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Fingerprint"
msgstr ""
msgstr "Otisak prsta"
#: src/components/alerts/alerts-sheet.tsx
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
@@ -542,6 +563,12 @@ msgstr "Za <0>{min}</0> {min, plural, one {minutu} other {minute}}"
msgid "Forgot password?"
msgstr "Zaboravljena lozinka?"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr "FreeBSD naredba"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -553,6 +580,10 @@ msgstr "Puna"
msgid "General"
msgstr "Općenito"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "GPU motori"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr ""
@@ -616,7 +647,7 @@ msgstr ""
#. Short label for load average
#: src/components/systems-table/systems-table-columns.tsx
msgid "Load Avg"
msgstr ""
msgstr "Prosječno opterećenje"
#: src/components/navbar.tsx
msgid "Log Out"
@@ -645,6 +676,7 @@ msgid "Manage display and notification preferences."
msgstr "Upravljajte postavkama prikaza i obavijesti."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr ""
@@ -680,6 +712,9 @@ msgid "Network traffic of docker containers"
msgstr "Mrežni promet Docker spremnika"
#: 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
msgid "Network traffic of public interfaces"
msgstr "Mrežni promet javnih sučelja"
@@ -694,7 +729,7 @@ msgstr "Nema rezultata."
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "No results."
msgstr ""
msgstr "Nema rezultata."
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
@@ -715,6 +750,10 @@ msgstr "Podrška za OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "Prilikom svakog ponovnog pokretanja, sustavi u bazi podataka biti će ažurirani kako bi odgovarali sustavima definiranim u datoteci."
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Jednokratna lozinka"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -737,7 +776,7 @@ msgstr "Stranica"
#. placeholder {1}: table.getPageCount()
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Page {0} of {1}"
msgstr ""
msgstr "Stranica {0} od {1}"
#: src/components/command-palette.tsx
msgid "Pages / Settings"
@@ -754,7 +793,7 @@ msgstr "Lozinka mora imati najmanje 8 znakova."
#: src/components/login/auth-form.tsx
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
msgid "Password reset request received"
@@ -770,7 +809,7 @@ msgstr "Pauzirano"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr ""
msgstr "Pauzirano ({pausedSystemsLength})"
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
@@ -833,6 +872,14 @@ msgstr "Pročitaj"
msgid "Received"
msgstr "Primljeno"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "Zatraži jednokratnu lozinku"
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr "Zatraži OTP"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "Resetiraj Lozinku"
@@ -888,10 +935,6 @@ msgstr "Poslano"
msgid "Set percentage thresholds for meter colors."
msgstr "Postavite pragove postotka za boje mjerača."
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr "Postavlja zadani vremenski raspon za grafikone kada se sustav gleda."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1002,6 +1045,10 @@ msgstr "Protok {extraFsName}"
msgid "Throughput of root filesystem"
msgstr "Protok root datotečnog sustava"
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr "Format vremena"
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "Primaoci e-pošte"
@@ -1034,6 +1081,14 @@ msgstr ""
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Ukupni podaci primljeni za svako sučelje"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr "Ukupni podaci poslani za svako sučelje"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr ""
@@ -1095,6 +1150,10 @@ msgstr ""
msgid "Up ({upSystemsLength})"
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "Otpremi"
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr "Vrijeme rada"
@@ -1122,12 +1181,16 @@ msgstr "Korisnici"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr ""
msgstr "Vrijednost"
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "Prikaz"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Prikaži više"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr ""
@@ -1174,7 +1237,7 @@ msgstr "Piši"
#: src/components/routes/settings/layout.tsx
msgid "YAML Config"
msgstr "YAML Config"
msgstr "YAML konfiguracija"
#: src/components/routes/settings/config-yaml.tsx
msgid "YAML Configuration"

View File

@@ -173,6 +173,10 @@ msgstr "Rendszerszintű CPU átlagos kihasználtság"
msgid "Average utilization of {0}"
msgstr "{0} átlagos kihasználtsága"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "GPU motorok átlagos kihasználtsága"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -363,6 +367,14 @@ msgstr ""
msgid "Critical (%)"
msgstr "Kritikus (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "Kumulatív letöltés"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "Kumulatív feltöltés"
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -441,6 +453,10 @@ msgstr ""
msgid "Down ({downSystemsLength})"
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr "Letöltés"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr ""
@@ -452,6 +468,7 @@ msgstr ""
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "Email"
@@ -472,6 +489,10 @@ msgstr "E-mail cím megadása a jelszó visszaállításához"
msgid "Enter email address..."
msgstr "Adja meg az e-mail címet..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "Adja meg az egyszeri jelszavát."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -542,6 +563,12 @@ msgstr "A <0>{min}</0> {min, plural, one {perc} other {percek}}"
msgid "Forgot password?"
msgstr "Elfelejtette a jelszavát?"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr "FreeBSD parancs"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -553,6 +580,10 @@ msgstr "Tele"
msgid "General"
msgstr "Általános"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "GPU motorok"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "GPU áramfelvétele"
@@ -645,6 +676,7 @@ msgid "Manage display and notification preferences."
msgstr "A megjelenítési és értesítési beállítások kezelése."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr ""
@@ -680,6 +712,9 @@ msgid "Network traffic of docker containers"
msgstr "Docker konténerek hálózati forgalma"
#: 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
msgid "Network traffic of public interfaces"
msgstr "Nyilvános interfészek hálózati forgalma"
@@ -715,6 +750,10 @@ msgstr "OAuth 2 / OIDC támogatás"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "Minden újraindításkor az adatbázisban lévő rendszerek frissítésre kerülnek, hogy megfeleljenek a fájlban meghatározott rendszereknek."
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Egyszeri jelszó"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -833,6 +872,14 @@ msgstr "Olvasás"
msgid "Received"
msgstr "Fogadott"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "Egyszeri jelszó kérése"
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr "OTP kérése"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "Jelszó visszaállítása"
@@ -888,10 +935,6 @@ msgstr "Elküldve"
msgid "Set percentage thresholds for meter colors."
msgstr "Százalékos küszöbértékek beállítása a mérőszínekhez."
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr "Beállítja az alapértelmezett időtartamot a diagramokhoz, amikor egy rendszert néznek."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1002,6 +1045,10 @@ msgstr "A {extraFsName} átviteli teljesítménye"
msgid "Throughput of root filesystem"
msgstr "A gyökér fájlrendszer átviteli teljesítménye"
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr "Időformátum"
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "E-mailben"
@@ -1034,6 +1081,14 @@ msgstr ""
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Összes fogadott adat minden interfészenként"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr "Összes elküldött adat minden interfészenként"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr ""
@@ -1095,6 +1150,10 @@ msgstr ""
msgid "Up ({upSystemsLength})"
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "Feltöltés"
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr "Üzemidő"
@@ -1128,6 +1187,10 @@ msgstr ""
msgid "View"
msgstr "Nézet"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Továbbiak megjelenítése"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr ""

View File

@@ -80,6 +80,10 @@ msgstr ""
msgid "Actions"
msgstr "Aðgerðir"
#: src/components/charts/systemd-services-table.tsx
msgid "active"
msgstr ""
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Active"
@@ -173,6 +177,10 @@ msgstr "Meðal nýting örgjörva yfir allt kerfið"
msgid "Average utilization of {0}"
msgstr "Meðal notkun af {0}"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -344,6 +352,7 @@ msgstr ""
msgid "CPU"
msgstr "Örgjörvi"
#: src/components/charts/systemd-services-table.tsx
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
#: src/lib/alerts.ts
@@ -363,6 +372,14 @@ msgstr ""
msgid "Critical (%)"
msgstr "Kritískt (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr ""
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -441,6 +458,10 @@ msgstr ""
msgid "Down ({downSystemsLength})"
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr ""
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr ""
@@ -452,6 +473,7 @@ msgstr ""
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "Netfang"
@@ -472,6 +494,10 @@ msgstr "Settu netfang til að endursetja lykilorð"
msgid "Enter email address..."
msgstr "Settu inn Netfang..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr ""
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -507,6 +533,10 @@ msgstr ""
msgid "Fahrenheit (°F)"
msgstr ""
#: src/components/charts/systemd-services-table.tsx
msgid "failed"
msgstr ""
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "Villa í auðkenningu"
@@ -542,6 +572,12 @@ msgstr ""
msgid "Forgot password?"
msgstr "Gleymt lykilorð?"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr ""
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -553,6 +589,10 @@ msgstr "Full"
msgid "General"
msgstr "Almennt"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr ""
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "Skjákorts rafmagnsnotkun"
@@ -645,6 +685,7 @@ msgid "Manage display and notification preferences."
msgstr ""
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr ""
@@ -653,6 +694,7 @@ msgstr ""
msgid "Max 1 min"
msgstr "Mest 1 mínúta"
#: src/components/charts/systemd-services-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Memory"
msgstr "Minni"
@@ -680,6 +722,9 @@ msgid "Network traffic of docker containers"
msgstr "Net traffík docker kerfa"
#: 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
msgid "Network traffic of public interfaces"
msgstr ""
@@ -688,6 +733,10 @@ msgstr ""
msgid "Network unit"
msgstr ""
#: src/components/charts/systemd-services-table.tsx
msgid "No failed services."
msgstr ""
#: src/components/command-palette.tsx
msgid "No results found."
msgstr "Engar niðurstöður fundust."
@@ -715,6 +764,10 @@ msgstr "OAuth 2 / OIDC stuðningur"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr ""
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -833,6 +886,14 @@ msgstr "Lesa"
msgid "Received"
msgstr "Móttekið"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr ""
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "Endurstilla lykilorð"
@@ -884,14 +945,18 @@ msgstr ""
msgid "Sent"
msgstr "Sent"
#: src/components/charts/systemd-services-table.tsx
msgid "Service"
msgstr ""
#: src/components/systems-table/systems-table-columns.tsx
msgid "Services"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Set percentage thresholds for meter colors."
msgstr "Stilltu prósentuþröskuld fyrir mælaliti."
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr ""
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -903,6 +968,14 @@ msgstr "Stillingar"
msgid "Settings saved"
msgstr "Stillingar vistaðar"
#: src/components/charts/systemd-services-table.tsx
msgid "Show all"
msgstr ""
#: src/components/charts/systemd-services-table.tsx
msgid "Show less"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Sign in"
msgstr "Innskrá"
@@ -920,6 +993,7 @@ msgstr "Raða eftir"
msgid "State"
msgstr ""
#: src/components/charts/systemd-services-table.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
@@ -944,6 +1018,11 @@ msgstr "Kerfi"
msgid "System load averages over time"
msgstr ""
#: src/components/charts/systemd-services-table.tsx
#: src/components/routes/system.tsx
msgid "Systemd Services"
msgstr ""
#: src/components/navbar.tsx
msgid "Systems"
msgstr "Kerfi"
@@ -1002,6 +1081,10 @@ msgstr ""
msgid "Throughput of root filesystem"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr ""
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "Til tölvupósta"
@@ -1034,6 +1117,14 @@ msgstr ""
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr ""
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr ""
@@ -1095,6 +1186,10 @@ msgstr ""
msgid "Up ({upSystemsLength})"
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr ""
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr ""
@@ -1128,6 +1223,10 @@ msgstr ""
msgid "View"
msgstr "Skoða"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr ""
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr ""

View File

@@ -173,6 +173,10 @@ msgstr "Utilizzo medio della CPU a livello di sistema"
msgid "Average utilization of {0}"
msgstr "Utilizzo medio di {0}"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "Utilizzo medio dei motori GPU"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -363,6 +367,14 @@ msgstr "Creato"
msgid "Critical (%)"
msgstr "Critico (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "Download cumulativo"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "Upload cumulativo"
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -441,6 +453,10 @@ msgstr "Offline"
msgid "Down ({downSystemsLength})"
msgstr "Offline ({downSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr "Scarica"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "Durata"
@@ -452,6 +468,7 @@ msgstr "Modifica"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "Email"
@@ -472,6 +489,10 @@ msgstr "Inserisci l'indirizzo email per reimpostare la password"
msgid "Enter email address..."
msgstr "Inserisci l'indirizzo email..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "Inserisci la tua password monouso."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -542,6 +563,12 @@ msgstr "Per <0>{min}</0> {min, plural, one {minuto} other {minuti}}"
msgid "Forgot password?"
msgstr "Password dimenticata?"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr "Comando FreeBSD"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -553,6 +580,10 @@ msgstr "Piena"
msgid "General"
msgstr "Generale"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "Motori GPU"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "Consumo della GPU"
@@ -645,6 +676,7 @@ msgid "Manage display and notification preferences."
msgstr "Gestisci le preferenze di visualizzazione e notifica."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr "Istruzioni di configurazione manuale"
@@ -680,6 +712,9 @@ msgid "Network traffic of docker containers"
msgstr "Traffico di rete dei container Docker"
#: 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
msgid "Network traffic of public interfaces"
msgstr "Traffico di rete delle interfacce pubbliche"
@@ -715,6 +750,10 @@ msgstr "Supporto OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "Ad ogni riavvio, i sistemi nel database verranno aggiornati per corrispondere ai sistemi definiti nel file."
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Password monouso"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -833,6 +872,14 @@ msgstr "Lettura"
msgid "Received"
msgstr "Ricevuto"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "Richiedi una password monouso"
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr "Richiedi OTP"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "Reimposta Password"
@@ -888,10 +935,6 @@ msgstr "Inviato"
msgid "Set percentage thresholds for meter colors."
msgstr "Imposta le soglie percentuali per i colori dei contatori."
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr "Imposta l'intervallo di tempo predefinito per i grafici quando viene visualizzato un sistema."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1002,6 +1045,10 @@ msgstr "Throughput di {extraFsName}"
msgid "Throughput of root filesystem"
msgstr "Throughput del filesystem root"
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr "Formato orario"
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "A email(s)"
@@ -1034,6 +1081,14 @@ msgstr "I token consentono agli agenti di connettersi e registrarsi. Le impronte
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "I token e le impronte digitali vengono utilizzati per autenticare le connessioni WebSocket all'hub."
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Dati totali ricevuti per ogni interfaccia"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr "Dati totali inviati per ogni interfaccia"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "Si attiva quando la media di carico di 1 minuto supera una soglia"
@@ -1095,6 +1150,10 @@ msgstr "Attivo"
msgid "Up ({upSystemsLength})"
msgstr "Attivo ({upSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "Carica"
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr "Tempo di attività"
@@ -1128,6 +1187,10 @@ msgstr "Valore"
msgid "View"
msgstr "Vista"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Visualizza altro"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr "Visualizza i tuoi 200 avvisi più recenti."

View File

@@ -173,6 +173,10 @@ msgstr "システム全体の平均CPU使用率"
msgid "Average utilization of {0}"
msgstr "{0}の平均使用率"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "GPUエンジンの平均使用率"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -363,6 +367,14 @@ msgstr "作成日"
msgid "Critical (%)"
msgstr "致命的 (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "累積ダウンロード"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "累積アップロード"
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -441,6 +453,10 @@ msgstr "停止"
msgid "Down ({downSystemsLength})"
msgstr "停止 ({downSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr "ダウンロード"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "期間"
@@ -452,6 +468,7 @@ msgstr "編集"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "メール"
@@ -472,6 +489,10 @@ msgstr "パスワードをリセットするためにメールアドレスを入
msgid "Enter email address..."
msgstr "メールアドレスを入力..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "ワンタイムパスワードを入力してください。"
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -542,6 +563,12 @@ msgstr "<0>{min}</0> {min, plural, one {分} other {分}}の間"
msgid "Forgot password?"
msgstr "パスワードをお忘れですか?"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr "FreeBSD コマンド"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -553,6 +580,10 @@ msgstr "満充電"
msgid "General"
msgstr "一般"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "GPUエンジン"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "GPUの消費電力"
@@ -645,6 +676,7 @@ msgid "Manage display and notification preferences."
msgstr "表示と通知の設定を管理します。"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr "手動セットアップの手順"
@@ -680,6 +712,9 @@ msgid "Network traffic of docker containers"
msgstr "Dockerコンテナのネットワークトラフィック"
#: 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
msgid "Network traffic of public interfaces"
msgstr "パブリックインターフェースのネットワークトラフィック"
@@ -715,6 +750,10 @@ msgstr "OAuth 2 / OIDCサポート"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "再起動のたびに、データベース内のシステムはファイルに定義されたシステムに一致するように更新されます。"
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "ワンタイムパスワード"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -833,6 +872,14 @@ msgstr "読み取り"
msgid "Received"
msgstr "受信"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "ワンタイムパスワードをリクエスト"
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr "OTP をリクエスト"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "パスワードをリセット"
@@ -888,10 +935,6 @@ msgstr "送信"
msgid "Set percentage thresholds for meter colors."
msgstr "メーターの色を変更するしきい値(パーセンテージ)を設定します。"
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr "システムを表示する際のチャートのデフォルトの時間範囲を設定します。"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1002,6 +1045,10 @@ msgstr "{extraFsName}のスループット"
msgid "Throughput of root filesystem"
msgstr "ルートファイルシステムのスループット"
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr "時間形式"
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "宛先メールアドレス"
@@ -1034,6 +1081,14 @@ msgstr "トークンはエージェントの接続と登録を可能にします
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "トークンとフィンガープリントは、ハブへのWebSocket接続の認証に使用されます。"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "各インターフェースの総受信データ量"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr "各インターフェースの総送信データ量"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "1分間の負荷平均がしきい値を超えたときにトリガーされます"
@@ -1095,6 +1150,10 @@ msgstr "正常"
msgid "Up ({upSystemsLength})"
msgstr "正常 ({upSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "アップロード"
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr "稼働時間"
@@ -1128,6 +1187,10 @@ msgstr "値"
msgid "View"
msgstr "表示"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "もっと見る"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr "直近200件のアラートを表示します。"

View File

@@ -8,15 +8,15 @@ msgstr ""
"Language: ko\n"
"Project-Id-Version: beszel\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"
"Language-Team: Korean\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Crowdin-Project: beszel\n"
"X-Crowdin-Project-ID: 733311\n"
"X-Crowdin-Language: ko\n"
"X-Crowdin-File: /main/beszel/site/src/locales/en/en.po\n"
"X-Crowdin-File-ID: 16\n"
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
"X-Crowdin-File-ID: 32\n"
#. placeholder {0}: Math.trunc(system.info?.u / 86400)
#: src/components/routes/system.tsx
@@ -173,6 +173,10 @@ msgstr "시스템 전체의 평균 CPU 사용량"
msgid "Average utilization of {0}"
msgstr "평균 {0} 사용량"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "GPU 엔진 평균 사용량"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -363,6 +367,14 @@ msgstr "생성됨"
msgid "Critical (%)"
msgstr "위험 (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "누적 다운로드"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "누적 업로드"
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -441,6 +453,10 @@ msgstr "오프라인"
msgid "Down ({downSystemsLength})"
msgstr "오프라인 ({downSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr "다운로드"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "기간"
@@ -452,6 +468,7 @@ msgstr "수정"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "이메일"
@@ -472,6 +489,10 @@ msgstr "비밀번호를 재설정하려면 이메일 주소를 입력하세요"
msgid "Enter email address..."
msgstr "이메일 주소 입력..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "OTP를 입력하세요."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -542,6 +563,12 @@ msgstr "<0>{min}</0> {min, plural, one {분} other {분}} 동안"
msgid "Forgot password?"
msgstr "비밀번호를 잊으셨나요?"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr "FreeBSD 명령어"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -553,6 +580,10 @@ msgstr "가득"
msgid "General"
msgstr "일반"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "GPU 엔진들"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "GPU 전원 사용량"
@@ -645,6 +676,7 @@ msgid "Manage display and notification preferences."
msgstr "디스플레이 및 알림 설정"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr "수동 설정 방법"
@@ -680,6 +712,9 @@ msgid "Network traffic of docker containers"
msgstr "Docker 컨테이너의 네트워크 트래픽"
#: 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
msgid "Network traffic of public interfaces"
msgstr "공용 인터페이스의 네트워크 트래픽"
@@ -715,6 +750,10 @@ msgstr "OAuth 2 / OIDC 지원"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "매 시작 시, 데이터베이스가 파일에 정의된 시스템과 일치하도록 업데이트됩니다."
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "OTP"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -833,6 +872,14 @@ msgstr "읽기"
msgid "Received"
msgstr "수신됨"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "OTP 요청"
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr "OTP 요청"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "비밀번호 재설정"
@@ -888,10 +935,6 @@ msgstr "보냄"
msgid "Set percentage thresholds for meter colors."
msgstr "그래프 미터 색상의 백분율 임계값을 설정합니다."
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr "시스템을 볼 때 차트의 기본 시간 범위를 설정합니다."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1002,6 +1045,10 @@ msgstr "{extraFsName}의 처리량"
msgid "Throughput of root filesystem"
msgstr "루트 파일 시스템의 처리량"
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr "시간 형식"
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "받는사람(들)"
@@ -1034,6 +1081,14 @@ msgstr "토큰은 에이전트가 연결하고 등록할 수 있도록 합니다
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "토큰과 지문은 허브에 대한 WebSocket 연결을 인증하는 데 사용됩니다."
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "각 인터페이스별 총합 다운로드 데이터량"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr "각 인터페이스별 총합 업로드 데이터량"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "1분 부하 평균이 임계값을 초과하면 트리거됩니다."
@@ -1095,6 +1150,10 @@ msgstr "온라인"
msgid "Up ({upSystemsLength})"
msgstr "온라인 ({upSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "업로드"
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr "가동 시간"
@@ -1128,6 +1187,10 @@ msgstr "값"
msgid "View"
msgstr "보기"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "더 보기"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr "최근 200개의 알림을 봅니다."

View File

@@ -173,6 +173,10 @@ msgstr "Gemiddeld systeembrede CPU-gebruik"
msgid "Average utilization of {0}"
msgstr "Gemiddeld gebruik van {0}"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "Gemiddeld gebruik van GPU-engines"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -363,6 +367,14 @@ msgstr "Aangemaakt"
msgid "Critical (%)"
msgstr "Kritiek (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "Cumulatieve download"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "Cumulatieve upload"
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -441,6 +453,10 @@ msgstr "Offline"
msgid "Down ({downSystemsLength})"
msgstr "Offline ({downSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr "Downloaden"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "Duur"
@@ -452,6 +468,7 @@ msgstr "Bewerken"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "E-mail"
@@ -472,6 +489,10 @@ msgstr "Voer een e-mailadres in om het wachtwoord opnieuw in te stellen"
msgid "Enter email address..."
msgstr "Voer een e-mailadres in..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "Voer uw eenmalig wachtwoord in."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -542,6 +563,12 @@ msgstr "Voor <0>{min}</0> {min, plural, one {minuut} other {minuten}}"
msgid "Forgot password?"
msgstr "Wachtwoord vergeten?"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr "FreeBSD commando"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -553,6 +580,10 @@ msgstr "Vol"
msgid "General"
msgstr "Algemeen"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "GPU-engines"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "GPU stroomverbruik"
@@ -645,6 +676,7 @@ msgid "Manage display and notification preferences."
msgstr "Weergave- en notificatievoorkeuren beheren."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr "Handmatige installatie-instructies"
@@ -680,6 +712,9 @@ msgid "Network traffic of docker containers"
msgstr "Netwerkverkeer van docker containers"
#: 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
msgid "Network traffic of public interfaces"
msgstr "Netwerkverkeer van publieke interfaces"
@@ -715,6 +750,10 @@ msgstr "OAuth 2 / OIDC ondersteuning"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "Bij elke herstart zullen systemen in de database worden bijgewerkt om overeen te komen met de systemen die in het bestand zijn gedefinieerd."
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Eenmalig wachtwoord"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -833,6 +872,14 @@ msgstr "Lezen"
msgid "Received"
msgstr "Ontvangen"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "Eenmalig wachtwoord aanvragen"
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr "OTP aanvragen"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "Wachtwoord resetten"
@@ -888,10 +935,6 @@ msgstr "Verzonden"
msgid "Set percentage thresholds for meter colors."
msgstr "Stel percentagedrempels in voor meterkleuren."
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr "Stelt het standaard tijdsbereik voor grafieken in wanneer een systeem wordt bekeken."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1002,6 +1045,10 @@ msgstr "Doorvoer van {extraFsName}"
msgid "Throughput of root filesystem"
msgstr "Doorvoer van het root bestandssysteem"
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr "Tijdnotatie"
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "Naar e-mail(s)"
@@ -1034,6 +1081,14 @@ msgstr "Tokens staan agenten toe om verbinding te maken met en te registreren. V
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Tokens en vingerafdrukken worden gebruikt om WebSocket verbindingen te verifiëren naar de hub."
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Totaal ontvangen gegevens per interface"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr "Totaal verzonden gegevens per interface"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "Triggert wanneer de gemiddelde belasting een drempelwaarde overschrijdt"
@@ -1095,6 +1150,10 @@ msgstr "Online"
msgid "Up ({upSystemsLength})"
msgstr "Online ({upSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "Uploaden"
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr "Actief"
@@ -1128,6 +1187,10 @@ msgstr "Waarde"
msgid "View"
msgstr "Weergave"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Meer weergeven"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr "Bekijk je 200 meest recente meldingen."

View File

@@ -173,6 +173,10 @@ msgstr "Gjennomsnittlig CPU-utnyttelse for hele systemet"
msgid "Average utilization of {0}"
msgstr "Gjennomsnittlig utnyttelse av {0}"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "Gjennomsnittlig utnyttelse av GPU-motorer"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -363,6 +367,14 @@ msgstr ""
msgid "Critical (%)"
msgstr "Kritisk (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "Kumulativ nedlasting"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "Kumulativ opplasting"
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -441,6 +453,10 @@ msgstr "Nede"
msgid "Down ({downSystemsLength})"
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr "Last ned"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr ""
@@ -452,6 +468,7 @@ msgstr "Rediger"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "E-post"
@@ -472,6 +489,10 @@ msgstr "Skriv inn e-postadresse for å nullstille passordet"
msgid "Enter email address..."
msgstr "Skriv inn e-postadresse..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "Skriv inn ditt engangspassord."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -542,6 +563,12 @@ msgstr "I <0>{min}</0> {min, plural, one {minutt} other {minutter}}"
msgid "Forgot password?"
msgstr "Glemt passord?"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr "FreeBSD kommando"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -553,6 +580,10 @@ msgstr "Full"
msgid "General"
msgstr "Generelt"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "GPU-motorer"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "GPU Effektforbruk"
@@ -645,6 +676,7 @@ msgid "Manage display and notification preferences."
msgstr "Endre visnings- og varslingsinnstillinger."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr "Instruks for Manuell Installasjon"
@@ -680,6 +712,9 @@ msgid "Network traffic of docker containers"
msgstr "Nettverkstrafikk av docker-konteinere"
#: 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
msgid "Network traffic of public interfaces"
msgstr "Nettverkstrafikk av eksterne nettverksgrensesnitt"
@@ -715,6 +750,10 @@ msgstr "OAuth 2 / OIDC-støtte"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "Ved hver omstart vil systemer i databasen bli oppdatert til å matche systemene definert i fila."
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Engangspassord"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -833,6 +872,14 @@ msgstr "Lesing"
msgid "Received"
msgstr "Mottatt"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "Be om engangspassord"
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr "Be om OTP"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "Nullstill Passord"
@@ -888,10 +935,6 @@ msgstr "Sendt"
msgid "Set percentage thresholds for meter colors."
msgstr "Angi prosentvise terskler for målerfarger."
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr "Angir standard tidsperiode for diagrammer når et system vises."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1002,6 +1045,10 @@ msgstr "Gjennomstrømning av {extraFsName}"
msgid "Throughput of root filesystem"
msgstr "Gjennomstrømning av rot-filsystemet"
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr "Tidsformat"
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "Til e-postadresse(r)"
@@ -1034,6 +1081,14 @@ msgstr ""
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Totalt mottatt data for hvert grensesnitt"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr "Totalt sendt data for hvert grensesnitt"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr ""
@@ -1095,6 +1150,10 @@ msgstr "Oppe"
msgid "Up ({upSystemsLength})"
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "Last opp"
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr "Oppetid"
@@ -1128,6 +1187,10 @@ msgstr ""
msgid "View"
msgstr "Visning"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Se mer"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr ""

View File

@@ -8,15 +8,15 @@ msgstr ""
"Language: pl\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-09-03 18:54\n"
"PO-Revision-Date: 2025-09-18 15:36\n"
"Last-Translator: \n"
"Language-Team: Polish\n"
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
"X-Crowdin-Project: beszel\n"
"X-Crowdin-Project-ID: 733311\n"
"X-Crowdin-Language: pl\n"
"X-Crowdin-File: /main/beszel/site/src/locales/en/en.po\n"
"X-Crowdin-File-ID: 16\n"
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
"X-Crowdin-File-ID: 32\n"
#. placeholder {0}: Math.trunc(system.info?.u / 86400)
#: src/components/routes/system.tsx
@@ -173,6 +173,10 @@ msgstr "Średnie wykorzystanie procesora w całym systemie"
msgid "Average utilization of {0}"
msgstr "Średnie użycie {0}"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "Średnie wykorzystanie silników GPU"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -363,6 +367,14 @@ msgstr "Utworzono"
msgid "Critical (%)"
msgstr "Krytyczny (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "Pobieranie skumulowane"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "Wysyłanie skumulowane"
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -441,6 +453,10 @@ msgstr "Nie działa"
msgid "Down ({downSystemsLength})"
msgstr "Nie działa ({downSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr "Pobieranie"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "Czas trwania"
@@ -452,6 +468,7 @@ msgstr "Edytuj"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "E-mail"
@@ -472,6 +489,10 @@ msgstr "Wprowadź adres e-mail, aby zresetować hasło"
msgid "Enter email address..."
msgstr "Wprowadź adres e-mail..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "Wprowadź swoje jednorazowe hasło."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -542,6 +563,12 @@ msgstr "Na <0>{min}</0> {min, plural, one {minutę} other {minut}}"
msgid "Forgot password?"
msgstr "Zapomniałeś hasła?"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr "Polecenie FreeBSD"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -553,6 +580,10 @@ msgstr "Pełna"
msgid "General"
msgstr "Ogólne"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "Silniki GPU"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "Moc GPU"
@@ -645,6 +676,7 @@ msgid "Manage display and notification preferences."
msgstr "Zarządzaj preferencjami wyświetlania i powiadomień."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr "Instrukcja ręcznej konfiguracji"
@@ -680,6 +712,9 @@ msgid "Network traffic of docker containers"
msgstr "Ruch sieciowy kontenerów Docker."
#: 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
msgid "Network traffic of public interfaces"
msgstr "Ruch sieciowy interfejsów publicznych"
@@ -715,6 +750,10 @@ msgstr "Wsparcie OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "Przy każdym ponownym uruchomieniu systemy w bazie danych będą aktualizowane, aby odpowiadały systemom zdefiniowanym w pliku."
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Hasło jednorazowe"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -827,12 +866,20 @@ msgstr "Klucz publiczny"
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
msgid "Read"
msgstr "Czytaj"
msgstr "Odczyt"
#: src/components/routes/system.tsx
msgid "Received"
msgstr "Otrzymane"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "Zażądaj jednorazowego hasła"
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr "Zażądaj OTP"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "Resetuj hasło"
@@ -888,10 +935,6 @@ msgstr "Wysłane"
msgid "Set percentage thresholds for meter colors."
msgstr "Ustaw progi procentowe dla kolorów mierników."
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr "Ustawia domyślny zakres czasowy dla wykresów, gdy system jest wyświetlony."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1002,6 +1045,10 @@ msgstr "Przepustowość {extraFsName}"
msgid "Throughput of root filesystem"
msgstr "Przepustowość głównego systemu plików"
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr "Format czasu"
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "Do e-mail(ów)"
@@ -1034,6 +1081,14 @@ msgstr "Tokeny umożliwiają agentom łączenie się i rejestrację. Odciski pal
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Tokeny i odciski palców (fingerprinty) służą do uwierzytelniania połączeń WebSocket z hubem."
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Całkowita ilość danych odebranych dla każdego interfejsu"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr "Całkowita ilość danych wysłanych dla każdego interfejsu"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "Uruchamia się, gdy 1-minutowe średnie obciążenie systemu przekroczy ustawiony próg"
@@ -1095,6 +1150,10 @@ msgstr "Działa"
msgid "Up ({upSystemsLength})"
msgstr "Działa ({upSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "Wysyłanie"
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr "Czas pracy"
@@ -1128,6 +1187,10 @@ msgstr "Wartość"
msgid "View"
msgstr "Widok"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Zobacz więcej"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr "Wyświetl 200 ostatnich alertów."

View File

@@ -173,6 +173,10 @@ msgstr "Utilização média de CPU em todo o sistema"
msgid "Average utilization of {0}"
msgstr "Utilização média de {0}"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "Utilização média dos motores GPU"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -363,6 +367,14 @@ msgstr "Criado"
msgid "Critical (%)"
msgstr "Crítico (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "Download cumulativo"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "Upload cumulativo"
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -441,6 +453,10 @@ msgstr "“Desligado”"
msgid "Down ({downSystemsLength})"
msgstr "Inativo ({downSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr "Transferir"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "Duração"
@@ -452,6 +468,7 @@ msgstr "Editar"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "Email"
@@ -472,6 +489,10 @@ msgstr "Digite o endereço de email para redefinir a senha"
msgid "Enter email address..."
msgstr "Digite o endereço de email..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "Insira a sua senha de uso único."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -542,6 +563,12 @@ msgstr "Por <0>{min}</0> {min, plural, one {minuto} other {minutos}}"
msgid "Forgot password?"
msgstr "Esqueceu a senha?"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr "Comando FreeBSD"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -553,6 +580,10 @@ msgstr "Cheia"
msgid "General"
msgstr "Geral"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "Motores GPU"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "Consumo de Energia da GPU"
@@ -645,6 +676,7 @@ msgid "Manage display and notification preferences."
msgstr "Gerenciar preferências de exibição e notificação."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr "Instruções de configuração manual"
@@ -680,6 +712,9 @@ msgid "Network traffic of docker containers"
msgstr "Tráfego de rede dos contêineres Docker"
#: 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
msgid "Network traffic of public interfaces"
msgstr "Tráfego de rede das interfaces públicas"
@@ -715,6 +750,10 @@ msgstr "Suporte a OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "A cada reinício, os sistemas no banco de dados serão atualizados para corresponder aos sistemas definidos no arquivo."
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Senha de uso único"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -833,6 +872,14 @@ msgstr "Ler"
msgid "Received"
msgstr "Recebido"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "Solicitar senha de uso único"
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr "Solicitar OTP"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "Redefinir Senha"
@@ -888,10 +935,6 @@ msgstr "Enviado"
msgid "Set percentage thresholds for meter colors."
msgstr "Defina os limiares de porcentagem para as cores do medidor."
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr "Define o intervalo de tempo padrão para gráficos quando um sistema é visualizado."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1002,6 +1045,10 @@ msgstr "Taxa de transferência de {extraFsName}"
msgid "Throughput of root filesystem"
msgstr "Taxa de transferência do sistema de arquivos raiz"
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr "Formato de hora"
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "Para email(s)"
@@ -1034,6 +1081,14 @@ msgstr "Os tokens permitem que os agentes se conectem e registrem. As impressõe
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Tokens e impressões digitais são usados para autenticar conexões WebSocket ao hub."
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Dados totais recebidos para cada interface"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr "Dados totais enviados para cada interface"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "Dispara quando a média de carga de 1 minuto excede um limite"
@@ -1095,6 +1150,10 @@ msgstr "“Ligado”"
msgid "Up ({upSystemsLength})"
msgstr "Ativo ({upSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "Carregar"
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr "Tempo de Atividade"
@@ -1128,6 +1187,10 @@ msgstr "Valor"
msgid "View"
msgstr "Visual"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Ver mais"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr "Veja os seus 200 alertas mais recentes."

View File

@@ -173,6 +173,10 @@ msgstr "Среднее использование CPU по всей систем
msgid "Average utilization of {0}"
msgstr "Среднее использование {0}"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "Средняя загрузка GPU движков"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -363,6 +367,14 @@ msgstr "Создано"
msgid "Critical (%)"
msgstr "Критический (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "Совокупная загрузка"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "Совокупная выгрузка"
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -441,6 +453,10 @@ msgstr "Не в сети"
msgid "Down ({downSystemsLength})"
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr "Скачать"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "Длительность"
@@ -452,6 +468,7 @@ msgstr "Редактировать"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "Электронная почта"
@@ -472,6 +489,10 @@ msgstr "Введите адрес электронной почты для сб
msgid "Enter email address..."
msgstr "Введите адрес электронной почты..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "Введите ваш одноразовый пароль."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -542,6 +563,12 @@ msgstr "На <0>{min}</0> {min, plural, one {минуту} other {минут}}"
msgid "Forgot password?"
msgstr "Забыли пароль?"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr "Команда FreeBSD"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -553,6 +580,10 @@ msgstr "Полная"
msgid "General"
msgstr "Общие"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "GPU движки"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "Потребляемая мощность GPU"
@@ -645,6 +676,7 @@ msgid "Manage display and notification preferences."
msgstr "Управляйте предпочтениями отображения и уведомлений."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr "Инструкции по ручной настройке"
@@ -680,6 +712,9 @@ msgid "Network traffic of docker containers"
msgstr "Сетевой трафик контейнеров Docker"
#: 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
msgid "Network traffic of public interfaces"
msgstr "Сетевой трафик публичных интерфейсов"
@@ -715,6 +750,10 @@ msgstr "Поддержка OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "При каждом перезапуске системы в базе данных будут обновлены в соответствии с системами, определенными в файле."
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Одноразовый пароль"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -833,6 +872,14 @@ msgstr "Чтение"
msgid "Received"
msgstr "Получено"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "Запросить одноразовый пароль"
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr "Запросить OTP"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "Сбросить пароль"
@@ -888,10 +935,6 @@ msgstr "Отправлено"
msgid "Set percentage thresholds for meter colors."
msgstr "Установите процентные пороги для цветов счетчиков."
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr "Устанавливает диапазон времени по умолчанию для графиков при просмотре системы."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1002,6 +1045,10 @@ msgstr "Пропускная способность {extraFsName}"
msgid "Throughput of root filesystem"
msgstr "Пропускная способность корневой файловой системы"
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr "Формат времени"
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "На электронную почту"
@@ -1034,6 +1081,14 @@ msgstr "Токены позволяют агентам подключаться
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Токены и отпечатки используются для аутентификации соединений WebSocket с хабом."
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Общий объем полученных данных для каждого интерфейса"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr "Общий объем отправленных данных для каждого интерфейса"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "Срабатывает, когда средняя загрузка за 1 минуту превышает порог"
@@ -1095,6 +1150,10 @@ msgstr "В сети"
msgid "Up ({upSystemsLength})"
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "Загрузить"
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr "Время работы"
@@ -1128,6 +1187,10 @@ msgstr "Значение"
msgid "View"
msgstr "Вид"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Показать больше"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr "Просмотреть 200 последних оповещений."

View File

@@ -173,6 +173,10 @@ msgstr "Povprečna CPU izkoriščenost v celotnem sistemu"
msgid "Average utilization of {0}"
msgstr "Povprečna poraba {0}"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "Povprečna uporaba GPU motorjev"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -363,6 +367,14 @@ msgstr ""
msgid "Critical (%)"
msgstr "Kritično (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "Kumulativno prenos"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "Kumulativno nalaganje"
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -441,6 +453,10 @@ msgstr ""
msgid "Down ({downSystemsLength})"
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr "Prenesi"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr ""
@@ -452,6 +468,7 @@ msgstr ""
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "E-pošta"
@@ -472,6 +489,10 @@ msgstr "Vnesite e-poštni naslov za ponastavitev gesla"
msgid "Enter email address..."
msgstr "Vnesite e-poštni naslov..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "Vnesite svoje enkratno geslo."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -542,6 +563,12 @@ msgstr "Za <0>{min}</0> {min, plural, one {minuto} other {minut}}"
msgid "Forgot password?"
msgstr "Pozabljeno geslo?"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr "Ukaz FreeBSD"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -553,6 +580,10 @@ msgstr "Polna"
msgid "General"
msgstr "Splošno"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "GPU motorji"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "GPU poraba moči"
@@ -645,6 +676,7 @@ msgid "Manage display and notification preferences."
msgstr "Upravljajte nastavitve prikaza in obvestil."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr ""
@@ -680,6 +712,9 @@ msgid "Network traffic of docker containers"
msgstr "Omrežni promet docker kontejnerjev"
#: 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
msgid "Network traffic of public interfaces"
msgstr "Omrežni promet javnih vmesnikov"
@@ -715,6 +750,10 @@ msgstr "Podpora za OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "Ob vsakem ponovnem zagonu bodo sistemi v zbirki podatkov posodobljeni, da se bodo ujemali s sistemi, definiranimi v datoteki."
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Enkratno geslo"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -833,6 +872,14 @@ msgstr "Preberano"
msgid "Received"
msgstr "Prejeto"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "Zahtevaj enkratno geslo"
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr "Zahtevaj OTP"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "Ponastavi geslo"
@@ -888,10 +935,6 @@ msgstr "Poslano"
msgid "Set percentage thresholds for meter colors."
msgstr "Nastavite odstotne pragove za barve merilnikov."
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr "Nastavi privzeti časovni obseg za grafikone, ko si ogledujete sistem."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1002,6 +1045,10 @@ msgstr "Prepustnost {extraFsName}"
msgid "Throughput of root filesystem"
msgstr "Prepustnost korenskega datotečnega sistema"
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr "Oblika časa"
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "E-pošta za"
@@ -1034,6 +1081,14 @@ msgstr ""
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Skupni prejeti podatki za vsak vmesnik"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr "Skupni poslani podatki za vsak vmesnik"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr ""
@@ -1095,6 +1150,10 @@ msgstr ""
msgid "Up ({upSystemsLength})"
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "Naloži"
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr "Čas delovanja"
@@ -1128,6 +1187,10 @@ msgstr ""
msgid "View"
msgstr "Pogled"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Prikaži več"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr ""

View File

@@ -173,6 +173,10 @@ msgstr "Genomsnittlig systemomfattande CPU-användning"
msgid "Average utilization of {0}"
msgstr "Genomsnittlig användning av {0}"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "Genomsnittlig användning av GPU-motorer"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -363,6 +367,14 @@ msgstr "Skapad"
msgid "Critical (%)"
msgstr "Kritisk (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "Kumulativ nedladdning"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "Kumulativ uppladdning"
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -441,6 +453,10 @@ msgstr ""
msgid "Down ({downSystemsLength})"
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr "Ladda ner"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr ""
@@ -452,6 +468,7 @@ msgstr ""
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "E-post"
@@ -472,6 +489,10 @@ msgstr "Ange e-postadress för att återställa lösenord"
msgid "Enter email address..."
msgstr "Ange e-postadress..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "Ange ditt engångslösenord."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -542,6 +563,12 @@ msgstr "Under <0>{min}</0> {min, plural, one {minut} other {minuter}}"
msgid "Forgot password?"
msgstr "Glömt lösenordet?"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr "FreeBSD kommando"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -553,6 +580,10 @@ msgstr "Full"
msgid "General"
msgstr "Allmänt"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "GPU-motorer"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "GPU-strömförbrukning"
@@ -645,6 +676,7 @@ msgid "Manage display and notification preferences."
msgstr "Hantera visnings- och aviseringsinställningar."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr ""
@@ -680,6 +712,9 @@ msgid "Network traffic of docker containers"
msgstr "Nätverkstrafik för dockercontainrar"
#: 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
msgid "Network traffic of public interfaces"
msgstr "Nätverkstrafik för publika gränssnitt"
@@ -715,6 +750,10 @@ msgstr "Stöd för OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "Vid varje omstart kommer systemen i databasen att uppdateras för att matcha systemen som definieras i filen."
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Engångslösenord"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -833,6 +872,14 @@ msgstr "Läs"
msgid "Received"
msgstr "Mottaget"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "Begär engångslösenord"
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr "Begär OTP"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "Återställ lösenord"
@@ -888,10 +935,6 @@ msgstr "Skickat"
msgid "Set percentage thresholds for meter colors."
msgstr "Ställ in procentuella tröskelvärden för mätarfärger."
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr "Anger standardtidsintervallet för diagram när ett system visas."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1002,6 +1045,10 @@ msgstr "Genomströmning av {extraFsName}"
msgid "Throughput of root filesystem"
msgstr "Genomströmning av rotfilsystemet"
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr "Tidsformat"
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "Till e-postadress(er)"
@@ -1034,6 +1081,14 @@ msgstr ""
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Totalt mottagen data för varje gränssnitt"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr "Totalt skickad data för varje gränssnitt"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr ""
@@ -1095,6 +1150,10 @@ msgstr ""
msgid "Up ({upSystemsLength})"
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "Ladda upp"
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr "Drifttid"
@@ -1128,6 +1187,10 @@ msgstr ""
msgid "View"
msgstr "Visa"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Visa mer"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr ""

View File

@@ -173,6 +173,10 @@ msgstr "Sistem genelinde ortalama CPU kullanımı"
msgid "Average utilization of {0}"
msgstr "{0} ortalama kullanımı"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "GPU motorlarının ortalama kullanımı"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -363,6 +367,14 @@ msgstr "Oluşturuldu"
msgid "Critical (%)"
msgstr "Kritik (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "Kümülatif İndirme"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "Kümülatif Yükleme"
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -441,6 +453,10 @@ msgstr "Kapalı"
msgid "Down ({downSystemsLength})"
msgstr "Kapalı ({downSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr "İndir"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "Süre"
@@ -452,6 +468,7 @@ msgstr "Düzenle"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "E-posta"
@@ -472,6 +489,10 @@ msgstr "Şifreyi sıfırlamak için e-posta adresini girin"
msgid "Enter email address..."
msgstr "E-posta adresini girin..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "Tek kullanımlık şifrenizi girin."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -542,6 +563,12 @@ msgstr "<0>{min}</0> {min, plural, one {dakika} other {dakika}} için"
msgid "Forgot password?"
msgstr "Şifrenizi mi unuttunuz?"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr "FreeBSD komutu"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -553,6 +580,10 @@ msgstr "Dolu"
msgid "General"
msgstr "Genel"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "GPU motorları"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "GPU Güç Çekimi"
@@ -645,6 +676,7 @@ msgid "Manage display and notification preferences."
msgstr "Görüntüleme ve bildirim tercihlerini yönetin."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr "Manuel kurulum talimatları"
@@ -680,6 +712,9 @@ msgid "Network traffic of docker containers"
msgstr "Docker konteynerlerinin ağ trafiği"
#: 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
msgid "Network traffic of public interfaces"
msgstr "Genel arayüzlerin ağ trafiği"
@@ -715,6 +750,10 @@ msgstr "OAuth 2 / OIDC desteği"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "Her yeniden başlatmada, veritabanındaki sistemler dosyada tanımlanan sistemlerle eşleşecek şekilde güncellenecektir."
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Tek kullanımlık şifre"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -833,6 +872,14 @@ msgstr "Oku"
msgid "Received"
msgstr "Alındı"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "Tek kullanımlık şifre iste"
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr "OTP iste"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "Şifreyi Sıfırla"
@@ -888,10 +935,6 @@ msgstr "Gönderildi"
msgid "Set percentage thresholds for meter colors."
msgstr "Sayaç renkleri için yüzde eşiklerini ayarlayın."
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr "Bir sistem görüntülendiğinde grafikler için varsayılan zaman aralığını ayarlar."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1002,6 +1045,10 @@ msgstr "{extraFsName} verimliliği"
msgid "Throughput of root filesystem"
msgstr "Kök dosya sisteminin verimliliği"
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr "Zaman formatı"
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "E-posta(lar)a"
@@ -1034,6 +1081,14 @@ msgstr "Token'lar agentların bağlanıp kaydolmasına izin verir. Parmak izleri
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Token'lar ve parmak izleri hub'a WebSocket bağlantılarını doğrulamak için kullanılır."
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Her arayüz için alınan toplam veri"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr "Her arayüz için gönderilen toplam veri"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "1 dakikalık yük ortalaması bir eşiği aştığında tetiklenir"
@@ -1095,6 +1150,10 @@ msgstr "Açık"
msgid "Up ({upSystemsLength})"
msgstr "Açık ({upSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "Yükle"
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr "Çalışma Süresi"
@@ -1128,6 +1187,10 @@ msgstr "Değer"
msgid "View"
msgstr "Görüntüle"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Daha fazla göster"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr "En son 200 uyarınızı görüntüleyin."

View File

@@ -173,6 +173,10 @@ msgstr "Середнє використання CPU по всій системі
msgid "Average utilization of {0}"
msgstr "Середнє використання {0}"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "Середнє використання рушіїв GPU"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -363,6 +367,14 @@ msgstr "Створено"
msgid "Critical (%)"
msgstr "Критично (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "Кумулятивне завантаження"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "Кумулятивне відвантаження"
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -441,6 +453,10 @@ msgstr "Не працює"
msgid "Down ({downSystemsLength})"
msgstr "Не працює ({downSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr "Завантажити"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "Тривалість"
@@ -452,6 +468,7 @@ msgstr "Редагувати"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "Електронна пошта"
@@ -472,6 +489,10 @@ msgstr "Введіть адресу електронної пошти для с
msgid "Enter email address..."
msgstr "Введіть адресу електронної пошти..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "Введіть ваш одноразовий пароль."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -542,6 +563,12 @@ msgstr "Протягом <0>{min}</0> {min, plural, one {хвилини} other {
msgid "Forgot password?"
msgstr "Забули пароль?"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr "Команда FreeBSD"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -553,6 +580,10 @@ msgstr "Повна"
msgid "General"
msgstr "Загальні"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "Рушії GPU"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "Енергоспоживання GPU"
@@ -645,6 +676,7 @@ msgid "Manage display and notification preferences."
msgstr "Керуйте параметрами відображення та сповіщень."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr "Інструкції з ручного налаштування"
@@ -680,6 +712,9 @@ msgid "Network traffic of docker containers"
msgstr "Мережевий трафік контейнерів Docker"
#: 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
msgid "Network traffic of public interfaces"
msgstr "Мережевий трафік публічних інтерфейсів"
@@ -715,6 +750,10 @@ msgstr "Підтримка OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "При кожному перезапуску системи в базі даних будуть оновлені, щоб відповідати системам, визначеним у файлі."
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Одноразовий пароль"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -833,6 +872,14 @@ msgstr "Читання"
msgid "Received"
msgstr "Отримано"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "Запит одноразового пароля"
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr "Запит OTP"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "Скинути пароль"
@@ -888,10 +935,6 @@ msgstr "Відправлено"
msgid "Set percentage thresholds for meter colors."
msgstr "Встановіть відсоткові пороги для кольорів лічильників."
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr "Встановлює стандартний діапазон часу для графіків при перегляді системи."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1002,6 +1045,10 @@ msgstr "Пропускна здатність {extraFsName}"
msgid "Throughput of root filesystem"
msgstr "Пропускна здатність кореневої файлової системи"
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr "Формат часу"
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "На електронну пошту"
@@ -1034,6 +1081,14 @@ msgstr "Токени дозволяють агентам підключатис
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Токени та відбитки використовуються для автентифікації WebSocket з'єднань до хабу."
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Загальний обсяг отриманих даних для кожного інтерфейсу"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr "Загальний обсяг відправлених даних для кожного інтерфейсу"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "Спрацьовує, коли середнє навантаження за 1 хвилину перевищує поріг"
@@ -1095,6 +1150,10 @@ msgstr "Працює"
msgid "Up ({upSystemsLength})"
msgstr "Працює ({upSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "Відвантажити"
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr "Час роботи"
@@ -1128,6 +1187,10 @@ msgstr "Значення"
msgid "View"
msgstr "Вигляд"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Переглянути більше"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr "Переглянути 200 останніх сповіщень."

View File

@@ -173,6 +173,10 @@ msgstr "Sử dụng CPU trung bình toàn hệ thống"
msgid "Average utilization of {0}"
msgstr "Mức sử dụng trung bình của {0}"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "Mức sử dụng trung bình của động cơ GPU"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -363,6 +367,14 @@ msgstr "Đã tạo"
msgid "Critical (%)"
msgstr "Độ nghiêm trọng (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "Tải xuống tích lũy"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "Tải lên tích lũy"
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -441,6 +453,10 @@ msgstr "Mất kết nối"
msgid "Down ({downSystemsLength})"
msgstr "Mất kết nối ({downSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr "Tải xuống"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "Thời lượng"
@@ -452,6 +468,7 @@ msgstr "Chỉnh sửa"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "Email"
@@ -472,6 +489,10 @@ msgstr "Nhập địa chỉ email để đặt lại mật khẩu"
msgid "Enter email address..."
msgstr "Nhập địa chỉ email..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "Nhập mật khẩu một lần của bạn."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -542,6 +563,12 @@ msgstr "Trong <0>{min}</0> {min, plural, one {phút} other {phút}}"
msgid "Forgot password?"
msgstr "Quên mật khẩu?"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr "Lệnh FreeBSD"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -553,6 +580,10 @@ msgstr "Đầy pin"
msgid "General"
msgstr "Chung"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "Động cơ GPU"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "Mức tiêu thụ điện của GPU"
@@ -645,6 +676,7 @@ msgid "Manage display and notification preferences."
msgstr "Quản lý tùy chọn hiển thị và thông báo."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr "Hướng dẫn cài đặt thủ công"
@@ -680,6 +712,9 @@ msgid "Network traffic of docker containers"
msgstr "Lưu lượng mạng của các container Docker"
#: 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
msgid "Network traffic of public interfaces"
msgstr "Lưu lượng mạng của các giao diện công cộng"
@@ -715,6 +750,10 @@ msgstr "Hỗ trợ OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "Mỗi khi khởi động lại, các hệ thống trong cơ sở dữ liệu sẽ được cập nhật để khớp với các hệ thống được định nghĩa trong tệp."
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Mật khẩu một lần"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -833,6 +872,14 @@ msgstr "Đọc"
msgid "Received"
msgstr "Đã nhận"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "Yêu cầu mật khẩu một lần"
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr "Yêu cầu OTP"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "Đặt lại Mật khẩu"
@@ -888,10 +935,6 @@ msgstr "Đã gửi"
msgid "Set percentage thresholds for meter colors."
msgstr "Đặt ngưỡng cho màu sắc đồng hồ."
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr "Đặt phạm vi thời gian mặc định cho biểu đồ khi một hệ thống được xem."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1002,6 +1045,10 @@ msgstr "Thông lượng của {extraFsName}"
msgid "Throughput of root filesystem"
msgstr "Thông lượng của hệ thống tệp gốc"
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr "Định dạng thời gian"
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "Đến email(s)"
@@ -1034,6 +1081,14 @@ msgstr "Token cho phép các tác nhân kết nối và đăng ký. Vân tay là
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Token và vân tay được sử dụng để xác thực các kết nối WebSocket đến trung tâm."
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Tổng dữ liệu nhận được cho mỗi giao diện"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr "Tổng dữ liệu gửi đi cho mỗi giao diện"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "Kích hoạt khi tải trung bình 1 phút vượt quá ngưỡng"
@@ -1095,6 +1150,10 @@ msgstr "Hoạt động"
msgid "Up ({upSystemsLength})"
msgstr "Hoạt động ({upSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "Tải lên"
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr "Thời gian hoạt động"
@@ -1128,6 +1187,10 @@ msgstr "Giá trị"
msgid "View"
msgstr "Xem"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Xem thêm"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr "Xem 200 cảnh báo gần đây nhất của bạn."

View File

@@ -173,6 +173,10 @@ msgstr "系统范围内的平均 CPU 使用率"
msgid "Average utilization of {0}"
msgstr "{0} 平均利用率"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "GPU 引擎的平均利用率"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -363,6 +367,14 @@ msgstr "创建时间"
msgid "Critical (%)"
msgstr "临界 (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "累计下载"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "累计上传"
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -441,6 +453,10 @@ msgstr "离线"
msgid "Down ({downSystemsLength})"
msgstr "离线 ({downSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr "下载"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "持续时间"
@@ -452,6 +468,7 @@ msgstr "编辑"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "电子邮件"
@@ -472,6 +489,10 @@ msgstr "输入电子邮件地址以重置密码"
msgid "Enter email address..."
msgstr "输入电子邮件地址..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "输入您的一次性密码。"
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -542,6 +563,12 @@ msgstr "持续<0>{min}</0> {min, plural, one {分钟} other {分钟}}"
msgid "Forgot password?"
msgstr "忘记密码?"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr "FreeBSD 命令"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -553,6 +580,10 @@ msgstr "满电"
msgid "General"
msgstr "常规"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "GPU 引擎"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "GPU 功耗"
@@ -645,6 +676,7 @@ msgid "Manage display and notification preferences."
msgstr "管理显示和通知偏好。"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr "手动设置说明"
@@ -680,6 +712,9 @@ msgid "Network traffic of docker containers"
msgstr "Docker 容器的网络流量"
#: 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
msgid "Network traffic of public interfaces"
msgstr "公共接口的网络流量"
@@ -715,6 +750,10 @@ msgstr "支持 OAuth 2/OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "每次重启时,数据库中的系统将更新以匹配文件中定义的系统。"
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "一次性密码"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -833,6 +872,14 @@ msgstr "读取"
msgid "Received"
msgstr "接收"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "请求一次性密码"
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr "请求 OTP"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "重置密码"
@@ -888,10 +935,6 @@ msgstr "发送"
msgid "Set percentage thresholds for meter colors."
msgstr "设置仪表颜色的百分比阈值。"
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr "设置查看系统时图表的默认时间范围。"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1002,6 +1045,10 @@ msgstr "{extraFsName}的吞吐量"
msgid "Throughput of root filesystem"
msgstr "根文件系统的吞吐量"
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr "时间格式"
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "发送到电子邮件"
@@ -1034,6 +1081,14 @@ msgstr "令牌允许客户端连接和注册。指纹是每个系统唯一的稳
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "令牌与指纹用于验证到中心的 WebSocket 连接。"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "每个接口的总接收数据量"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr "每个接口的总发送数据量"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "当 1 分钟负载平均值超过阈值时触发"
@@ -1095,6 +1150,10 @@ msgstr "在线"
msgid "Up ({upSystemsLength})"
msgstr "在线 ({upSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "上传"
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr "正常运行时间"
@@ -1128,6 +1187,10 @@ msgstr "值"
msgid "View"
msgstr "视图"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "查看更多"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr "查看您最近的200个警报。"

View File

@@ -173,6 +173,10 @@ msgstr "系統的平均 CPU 使用率"
msgid "Average utilization of {0}"
msgstr "{0} 的平均使用率"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "GPU 引擎的平均利用率"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -363,6 +367,14 @@ msgstr "已建立"
msgid "Critical (%)"
msgstr "嚴重 (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "累計下載"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "累計上傳"
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -441,6 +453,10 @@ msgstr "中斷"
msgid "Down ({downSystemsLength})"
msgstr "中斷 ({downSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr "下載"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "持續時間"
@@ -452,6 +468,7 @@ msgstr "編輯"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "電子郵件"
@@ -472,6 +489,10 @@ msgstr "輸入電子郵件地址以重置密碼"
msgid "Enter email address..."
msgstr "輸入電子郵件地址..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "輸入您的一次性密碼。"
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -542,6 +563,12 @@ msgstr "持續<0>{min}</0> {min, plural, one {分鐘} other {分鐘}}"
msgid "Forgot password?"
msgstr "忘記密碼?"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr "FreeBSD 指令"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -553,6 +580,10 @@ msgstr "滿電"
msgid "General"
msgstr "一般"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "GPU 引擎"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "GPU 功耗"
@@ -645,6 +676,7 @@ msgid "Manage display and notification preferences."
msgstr "管理顯示和通知偏好。"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr "手動設定說明"
@@ -680,6 +712,9 @@ msgid "Network traffic of docker containers"
msgstr "Docker 容器的網絡流量"
#: 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
msgid "Network traffic of public interfaces"
msgstr "公共接口的網絡流量"
@@ -715,6 +750,10 @@ msgstr "支援 OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "每次重新啟動時,將會以檔案中的系統定義更新資料庫。"
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "一次性密碼"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -833,6 +872,14 @@ msgstr "讀取"
msgid "Received"
msgstr "接收"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "請求一次性密碼"
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr "請求 OTP"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "重設密碼"
@@ -888,10 +935,6 @@ msgstr "發送"
msgid "Set percentage thresholds for meter colors."
msgstr "設定儀表顏色的百分比閾值。"
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr "設置查看系統時圖表的默認時間範圍。"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1002,6 +1045,10 @@ msgstr "{extraFsName}的吞吐量"
msgid "Throughput of root filesystem"
msgstr "根文件系統的吞吐量"
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr "時間格式"
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "發送到電子郵件"
@@ -1034,6 +1081,14 @@ msgstr "令牌允許代理程式連接和註冊。指紋是每個系統唯一的
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "令牌和指紋用於驗證到中心的WebSocket連接。"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "每個介面的總接收資料量"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr "每個介面的總傳送資料量"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "當 1 分鐘平均負載超過閾值時觸發"
@@ -1095,6 +1150,10 @@ msgstr "上線"
msgid "Up ({upSystemsLength})"
msgstr "上線 ({upSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "上傳"
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr "正常運行時間"
@@ -1128,6 +1187,10 @@ msgstr "值"
msgid "View"
msgstr "檢視"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "查看更多"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr "檢視最近 200 則警報。"

View File

@@ -173,6 +173,10 @@ msgstr "系統的平均 CPU 使用率"
msgid "Average utilization of {0}"
msgstr "{0} 的平均使用率"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "GPU 引擎的平均利用率"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -363,6 +367,14 @@ msgstr "已建立"
msgid "Critical (%)"
msgstr "嚴重 (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "累計下載"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "累計上傳"
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -441,6 +453,10 @@ msgstr "離線"
msgid "Down ({downSystemsLength})"
msgstr "離線 ({downSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr "下載"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "持續時間"
@@ -452,6 +468,7 @@ msgstr "編輯"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "電子郵件"
@@ -472,6 +489,10 @@ msgstr "輸入電子郵件地址以重設密碼"
msgid "Enter email address..."
msgstr "輸入電子郵件地址..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "輸入您的一次性密碼。"
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -542,6 +563,12 @@ msgstr "持續<0>{min}</0> {min, plural, one {分鐘} other {分鐘}}"
msgid "Forgot password?"
msgstr "忘記密碼?"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr "FreeBSD 指令"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
@@ -553,6 +580,10 @@ msgstr "滿電"
msgid "General"
msgstr "一般"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "GPU 引擎"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "GPU 功耗"
@@ -645,6 +676,7 @@ msgid "Manage display and notification preferences."
msgstr "管理顯示和通知偏好。"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr "手動設定說明"
@@ -680,6 +712,9 @@ msgid "Network traffic of docker containers"
msgstr "Docker 容器的網路流量"
#: 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
msgid "Network traffic of public interfaces"
msgstr "公開介面的網路流量"
@@ -715,6 +750,10 @@ msgstr "支援 OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "每次重新啟動時,將會以檔案中的系統定義更新資料庫。"
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "一次性密碼"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -833,6 +872,14 @@ msgstr "讀取"
msgid "Received"
msgstr "接收"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "請求一次性密碼"
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr "請求 OTP"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "重設密碼"
@@ -888,10 +935,6 @@ msgstr "傳送"
msgid "Set percentage thresholds for meter colors."
msgstr "設定儀表顏色的百分比閾值。"
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr "設定顯示系統圖表的預設時間範圍。"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1002,6 +1045,10 @@ msgstr "{extraFsName}的傳輸速率"
msgid "Throughput of root filesystem"
msgstr "Root文件系統的傳輸速率"
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr "時間格式"
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "發送到電子郵件"
@@ -1034,6 +1081,14 @@ msgstr "令牌允許代理程式連線和註冊。指紋是每個系統的唯一
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "令牌和指紋被用於驗證到 Hub 的 WebSocket 連線。"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "每個介面的總接收資料量"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr "每個介面的總傳送資料量"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "當 1 分鐘平均負載超過閾值時觸發"
@@ -1095,6 +1150,10 @@ msgstr "上線"
msgid "Up ({upSystemsLength})"
msgstr "上線 ({upSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "上傳"
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr "運行時間"
@@ -1128,6 +1187,10 @@ msgstr "值"
msgid "View"
msgstr "檢視"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "查看更多"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr "檢視最近 200 則警報。"

View File

@@ -1,5 +1,5 @@
import type { RecordModel } from "pocketbase"
import type { Unit, Os, BatteryState, HourFormat } from "./lib/enums"
import type { Unit, Os, BatteryState, HourFormat, ConnectionType } from "@/lib/enums"
// global window properties
declare global {
@@ -75,6 +75,8 @@ export interface SystemInfo {
dt?: number
/** operating system */
os?: Os
/** connection type */
ct?: ConnectionType
}
export interface SystemStats {
@@ -141,6 +143,19 @@ export interface SystemStats {
g?: Record<string, GPUData>
/** battery percent and state */
bat?: [number, BatteryState]
/** network interfaces [upload bytes, download bytes, total upload bytes, total download bytes] */
ni?: Record<string, [number, number, number, number]>
}
export interface SystemdService {
/** name */
n: string
/** status */
s: string
/** cpu percent */
c: number
/** memory used (mb) */
m: number
}
export interface GPUData {
@@ -154,6 +169,10 @@ export interface GPUData {
u: number
/** power (w) */
p?: number
/** power package (w) */
pp?: number
/** engines */
e?: Record<string, number>
}
export interface ExtraFsStats {
@@ -177,6 +196,12 @@ export interface ContainerStatsRecord extends RecordModel {
created: string | number
}
export interface SystemdStatsRecord extends RecordModel {
system: string
stats: SystemdService[]
created: string | number
}
interface ContainerStats {
/** name */
n: string

Some files were not shown because too many files have changed in this diff Show More